Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5244a12422 | |||
| 5b30764fa8 | |||
| 85ed7c3d2f | |||
| 4d5b307a74 | |||
| 68566b556c | |||
| b0032c1745 | |||
| 35b7aab174 | |||
| 2601215c91 | |||
| 4ebf0cad6b | |||
| 98d959112c | |||
| d01c2afaa6 | |||
| 8ebcb55b0d | |||
| 97807494fd | |||
| c4eb2e2dc9 | |||
| f0c3bf18b0 | |||
| 3eaae66bc4 | |||
| 27d1cd405f | |||
| 267965a065 | |||
| 181aca0215 | |||
| b6eef0145f | |||
| a1bc421fea | |||
| 502b3894d7 | |||
| 8e9005d9c3 | |||
| 7978f84f6c | |||
| f1f578c6fb | |||
| aae7de8e05 | |||
| 4da61a77c7 | |||
| 4fbb25e385 |
+5
-11
@@ -1,13 +1,7 @@
|
||||
# Binaries (distributed via GitHub releases)
|
||||
*.exe
|
||||
*.app
|
||||
claude-mem
|
||||
claude-mem-*
|
||||
|
||||
# Temporary files
|
||||
*.tmp
|
||||
node_modules/
|
||||
*.log
|
||||
.DS_Store
|
||||
|
||||
# Node modules (if any)
|
||||
node_modules/
|
||||
.env
|
||||
.env.local
|
||||
*.tmp
|
||||
*.temp
|
||||
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
|
||||
|
||||
## [3.7.1] - 2025-09-17
|
||||
|
||||
### Added
|
||||
- SQLite storage backend with session, memory, overview, and diagnostics management
|
||||
- Mintlify documentation site with searchable interface and comprehensive guides
|
||||
- Context7 MCP integration for documentation retrieval
|
||||
|
||||
### Changed
|
||||
- Session-start overviews to display chronologically from oldest to newest
|
||||
|
||||
### Fixed
|
||||
- Migration index parsing bug that prevented JSONL records from importing to SQLite
|
||||
|
||||
|
||||
## [3.6.10] - 2025-09-16
|
||||
|
||||
### Added
|
||||
- Claude Code statusline integration for real-time memory status
|
||||
- MCP memory tools server providing compress, stats, search, and overview commands
|
||||
- Concept documentation explaining memory compression and context loading
|
||||
|
||||
### Fixed
|
||||
- Corrected integration architecture to use hooks instead of MCP SDK
|
||||
|
||||
|
||||
## [3.6.9] - 2025-09-14
|
||||
|
||||
### Added
|
||||
- Display current date and time at the top of session-start hook output for better temporal context
|
||||
|
||||
### Changed
|
||||
- Enhanced session-start hook formatting with emoji icons and separator lines for improved readability
|
||||
|
||||
|
||||
## [3.6.8] - 2025-09-14
|
||||
|
||||
### Fixed
|
||||
- Fixed publish command failing when no version-related memories exist for changelog generation
|
||||
|
||||
|
||||
## [3.6.6] - 2025-09-14
|
||||
|
||||
### Fixed
|
||||
- Resolved compaction errors when processing large conversation histories by reducing chunk size limits to stay within Claude's context window
|
||||
|
||||
|
||||
## [3.6.5] - 2025-09-14
|
||||
|
||||
### Changed
|
||||
- Session groups now display in chronological order (most recent first)
|
||||
|
||||
### Fixed
|
||||
- Improved CLI path detection for cross-platform compatibility
|
||||
|
||||
|
||||
## [3.6.4] - 2025-09-13
|
||||
|
||||
### Changed
|
||||
- Update save documentation to include allowed-tools and description metadata fields
|
||||
|
||||
### Removed
|
||||
- Remove deprecated markdown to JSONL migration script
|
||||
|
||||
|
||||
## [3.6.3] - 2025-09-11
|
||||
|
||||
### Changed
|
||||
- Updated changelog generation prompts to use date strings in query text for temporal filtering
|
||||
|
||||
### Fixed
|
||||
- Resolved changelog timestamp filtering by using semantic search instead of metadata queries, enabling proper date-based searches
|
||||
- Corrected install.ts search instructions to remove misleading metadata filtering guidance that caused 'Error finding id' errors
|
||||
|
||||
|
||||
## [3.6.2] - 2025-09-10
|
||||
|
||||
### Added
|
||||
- Visual feedback to changelog command showing current version, next version, and number of overviews being processed
|
||||
- Generate changelog for specific versions using `--generate` flag with npm publish time boundaries
|
||||
- Introduce 'Who Wants To Be a Memoryonaire?' trivia game that generates personalized questions from your stored memories
|
||||
- Add interactive terminal UI with lifelines (50:50, Phone-a-Friend, Audience Poll) and cross-platform audio support
|
||||
- Implement permanent question caching with --regenerate flag for instant game loading
|
||||
- Enable hybrid vector search to discover related memory chains during question generation
|
||||
|
||||
### Changed
|
||||
- Changelog regeneration automatically removes old entries from JSONL file when using `--generate` or `--historical` flags
|
||||
- Switch to direct JSONL file loading for instant memory access without API calls
|
||||
- Optimize AI generation with faster 'sonnet' model for improved performance
|
||||
- Reduce memory query limit from 100 to 50 to prevent token overflow
|
||||
|
||||
### Fixed
|
||||
- Changelog command now uses npm publish timestamps exclusively for accurate version time ranges
|
||||
- Resolved timestamp filtering issues with Chroma database by leveraging semantic search with embedded dates
|
||||
- Resolve game hanging at startup due to confirmation loop
|
||||
- Fix memory integration bypass that prevented questions from using actual stored memories
|
||||
- Consolidate 500+ lines of duplicate code for better maintainability
|
||||
|
||||
|
||||
## [3.6.1] - 2025-09-10
|
||||
|
||||
### Changed
|
||||
- Refactored pre-compact hook to work independently without status line updates
|
||||
|
||||
### Removed
|
||||
- Removed status line integration and ccstatusline configuration support
|
||||
|
||||
|
||||
## [3.5.5] - 2025-09-10
|
||||
|
||||
### Changed
|
||||
- Standardized GitHub release naming to lowercase 'claude-mem vX.X.X' format for consistent branding
|
||||
|
||||
@@ -1,43 +1,630 @@
|
||||
# Claude Mem License
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (c) 2024 Alex Newman (@thedotmack)
|
||||
Copyright (C) 2025 Alex Newman (@thedotmack). All rights reserved.
|
||||
|
||||
## Binary Distribution License
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
The compiled binaries (claude-mem, claude-mem.exe, etc.) are provided free of charge for personal and commercial use under the following terms:
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
1. **USE**: You may use the binaries for any purpose.
|
||||
2. **DISTRIBUTION**: You may redistribute the unmodified binaries.
|
||||
3. **NO REVERSE ENGINEERING**: You may not decompile, disassemble, or reverse engineer the binaries.
|
||||
4. **NO MODIFICATION**: You may not modify the binary files.
|
||||
5. **NO WARRANTY**: The software is provided "as is" without warranty of any kind.
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
## Hook Files License (MIT)
|
||||
Preamble
|
||||
|
||||
The hook files in the `/hooks` directory are licensed under the MIT License:
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server software.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of these hook files and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
our General Public Licenses are intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users.
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
## Contributions
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
software used on network servers, this result may fail to come about.
|
||||
The GNU General Public License permits making a modified version and
|
||||
letting the public access it on a server without ever releasing its
|
||||
source code to the public.
|
||||
|
||||
By submitting pull requests for hook files or documentation, you agree to license your contributions under the MIT License.
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
provide the source code of the modified version running there to the
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
|
||||
## Trademark
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
|
||||
"Claude Mem" is a trademark of Alex Newman. You may use the name when referring to this software, but not in a way that implies endorsement or affiliation.
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your version
|
||||
supports such interaction) an opportunity to receive the Corresponding
|
||||
Source of your version by providing access to the Corresponding Source
|
||||
from a network server at no charge, through some standard or customary
|
||||
means of facilitating copying of software. This Corresponding Source
|
||||
shall include the Corresponding Source for any work covered by version 3
|
||||
of the GNU General Public License that is incorporated pursuant to the
|
||||
following paragraph.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU Affero General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU Affero General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU Affero General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU Affero General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
@@ -1,215 +1,248 @@
|
||||
# Claude Memory System (claude-mem)
|
||||
# 🧠 Claude Memory System (claude-mem)
|
||||
|
||||
**Truth + Context = Clarity**
|
||||
A real-time memory system for Claude Code that captures, compresses, and retrieves conversation context across sessions using semantic search and vector embeddings.
|
||||
|
||||
A revolutionary memory system that transforms your Claude Code conversations into a persistent, intelligent knowledge base. Never lose valuable insights, code patterns, or debugging solutions again. Your AI assistant finally has a memory that spans across all your projects and sessions.
|
||||
## ⚡️ Quick Start
|
||||
|
||||
## 🚀 Why Claude-Mem?
|
||||
|
||||
### The Problem We Solve
|
||||
- **Lost Context**: Starting every Claude Code session from scratch
|
||||
- **Repeated Explanations**: Re-describing your codebase and architecture repeatedly
|
||||
- **Fragmented Knowledge**: Valuable insights scattered across hundreds of conversations
|
||||
- **Context Switching**: Losing progress when switching between projects or devices
|
||||
- **Knowledge Decay**: Brilliant solutions forgotten and re-discovered multiple times
|
||||
|
||||
### The Claude-Mem Solution
|
||||
Transform your Claude Code experience from forgetful to persistent, from isolated sessions to connected knowledge, from starting over to building upon previous insights.
|
||||
|
||||
## ✨ Key Features
|
||||
|
||||
### 🧠 **Intelligent Memory Compression**
|
||||
- Automatically extracts key learnings from your Claude Code conversations
|
||||
- Identifies patterns, architectural decisions, and breakthrough moments
|
||||
- Compresses hours of conversation into searchable, actionable knowledge
|
||||
- Uses advanced AI analysis to understand context and significance
|
||||
|
||||
### 🔄 **Seamless Integration**
|
||||
- **One-command setup**: `claude-mem install` and you're ready
|
||||
- **Zero friction**: Works invisibly in the background
|
||||
- **Automatic triggers**: Memory compression on `/compact` and `/clear`
|
||||
- **Instant context loading**: New sessions start with relevant memories
|
||||
|
||||
### 🎯 **Smart Context Loading**
|
||||
- Loads relevant memories when starting new sessions
|
||||
- Project-aware context selection
|
||||
- Semantic search finds related knowledge across all sessions
|
||||
- Prevents re-explaining the same concepts repeatedly
|
||||
|
||||
### 📚 **Comprehensive Knowledge Base**
|
||||
- Stores technical implementations, bug fixes, and solutions
|
||||
- Captures design patterns and architectural decisions
|
||||
- Remembers tool configurations and setup procedures
|
||||
- Archives complete conversation transcripts for detailed reference
|
||||
|
||||
### 🔍 **Powerful Search & Retrieval**
|
||||
- Vector-based semantic search finds related concepts
|
||||
- Keyword search for specific terms and technologies
|
||||
- Project filtering to focus on relevant memories
|
||||
- Time-based filtering to find recent insights
|
||||
|
||||
## 🛠 Installation & Setup
|
||||
|
||||
### Quick Install
|
||||
```bash
|
||||
# Install globally
|
||||
npm install -g claude-mem
|
||||
|
||||
# Set up Claude Code integration
|
||||
claude-mem install
|
||||
|
||||
# Restart Claude Code to activate
|
||||
```
|
||||
|
||||
### Alternative Installation
|
||||
Restart Claude Code. Memory capture starts automatically.
|
||||
|
||||
## ✨ What It Does
|
||||
|
||||
**Real-Time Memory Capture**
|
||||
- Captures every conversation turn as it happens via streaming hooks
|
||||
- User prompts stored immediately in ChromaDB with atomic facts
|
||||
- Tool responses compressed asynchronously via Agent SDK
|
||||
- Project-based memory isolation with hierarchical metadata
|
||||
- Automatic context loading at session start and `/clear`
|
||||
|
||||
**Semantic Search**
|
||||
- Vector embeddings for intelligent retrieval via ChromaDB
|
||||
- Find relevant context from past conversations
|
||||
- Project-aware memory queries with temporal filtering
|
||||
- Date-based search using query text (not metadata)
|
||||
- 15+ MCP tools for memory operations
|
||||
|
||||
**Invisible Operation**
|
||||
- Zero user configuration required
|
||||
- Memory compression happens in background via SDK
|
||||
- SDK transcripts auto-deleted from UI history
|
||||
- Session overviews generated automatically
|
||||
- Live memory viewer with SSE streaming
|
||||
|
||||
**Smart Trash™**
|
||||
- Safe deletion with easy recovery
|
||||
- Timestamped trash entries
|
||||
- One-command restore
|
||||
- Located at `~/.claude-mem/trash/`
|
||||
|
||||
## 🎯 Core Features
|
||||
|
||||
- **Streaming Hooks**: Real-time capture with minimal overhead (<50ms)
|
||||
- **Agent SDK Integration**: Async compression without blocking conversation
|
||||
- **MCP Server**: 15+ ChromaDB tools for memory operations
|
||||
- **Project Isolation**: Memories segregated by project context
|
||||
- **Zero Configuration**: Works out of the box after install
|
||||
- **Embedded Databases**: ChromaDB and SQLite, no external dependencies
|
||||
- **Invisible UX**: Memory operations don't pollute conversation UI
|
||||
- **Live Memory Viewer**: Real-time slideshow of memories via SSE
|
||||
|
||||
## 🧭 Commands
|
||||
|
||||
```bash
|
||||
# Use without installing globally
|
||||
npx claude-mem install
|
||||
```
|
||||
# Setup & Status
|
||||
claude-mem install # Install/repair hooks and MCP integration
|
||||
claude-mem status # Check installation and memory stats
|
||||
claude-mem doctor # Run environment and pipeline diagnostics
|
||||
claude-mem uninstall # Remove all hooks
|
||||
|
||||
### Verification
|
||||
```bash
|
||||
# Check installation status
|
||||
claude-mem status
|
||||
```
|
||||
# Memory Operations
|
||||
claude-mem load-context # View current session context
|
||||
claude-mem logs # View operation logs
|
||||
claude-mem changelog # Generate CHANGELOG.md from memories
|
||||
|
||||
## 💻 How It Works
|
||||
# Storage Operations (Used by hooks/SDK)
|
||||
claude-mem store-memory # Store a memory to ChromaDB + SQLite
|
||||
claude-mem store-overview # Store a session overview
|
||||
|
||||
### The Memory Lifecycle
|
||||
# Smart Trash™
|
||||
claude-mem trash # View trash contents
|
||||
claude-mem restore # Restore from trash
|
||||
claude-mem trash-empty # Permanently delete trash
|
||||
|
||||
1. **🎬 Session Start**: Claude-mem loads relevant context from your knowledge base
|
||||
2. **💬 Active Session**: You work normally in Claude Code - no changes needed
|
||||
3. **🗜️ Memory Compression**: Use `/compact` or `/clear` to trigger intelligent compression
|
||||
4. **🧠 Knowledge Extraction**: AI analysis extracts key learnings and patterns
|
||||
5. **💾 Persistent Storage**: Memories stored in searchable vector database
|
||||
6. **🔄 Context Ready**: Next session starts with relevant memories loaded
|
||||
|
||||
### Technical Architecture
|
||||
|
||||
- **Vector Database**: ChromaDB for semantic search and storage
|
||||
- **MCP Integration**: Model Context Protocol for Claude Code communication
|
||||
- **AI Analysis**: Advanced prompt engineering for knowledge extraction
|
||||
- **Local Storage**: All data stored locally in `~/.claude-mem/`
|
||||
|
||||
## 📋 Commands Reference
|
||||
|
||||
### Core Commands
|
||||
```bash
|
||||
claude-mem install # Set up Claude Code integration
|
||||
claude-mem status # Check system status and configuration
|
||||
claude-mem load-context # View and search stored memories
|
||||
claude-mem logs # View system logs and debug information
|
||||
claude-mem uninstall # Remove Claude Code hooks
|
||||
```
|
||||
|
||||
### Advanced Usage
|
||||
```bash
|
||||
claude-mem compress <file> # Manually compress a transcript file
|
||||
claude-mem restore # Restore from backups
|
||||
claude-mem trash-view # View deleted files (Smart Trash feature)
|
||||
# ChromaDB Tools (15+ MCP tools available)
|
||||
claude-mem chroma_* # Direct ChromaDB operations
|
||||
```
|
||||
|
||||
## 📁 Storage Structure
|
||||
|
||||
Your claude-mem data is organized in `~/.claude-mem/`:
|
||||
|
||||
```
|
||||
~/.claude-mem/
|
||||
├── index/ # ChromaDB vector database
|
||||
├── archives/ # Original conversation transcripts
|
||||
├── hooks/ # Claude Code integration scripts
|
||||
├── trash/ # Smart Trash (deleted files)
|
||||
└── logs/ # System logs and debug information
|
||||
├── chroma/ # ChromaDB vector database
|
||||
├── archives/ # Compressed transcript backups
|
||||
├── index/ # Legacy JSONL memory indices
|
||||
├── hooks/ # Hook configuration files
|
||||
├── trash/ # Smart Trash™ with recovery
|
||||
├── logs/ # Operation logs
|
||||
└── claude-mem.db # SQLite metadata database
|
||||
```
|
||||
|
||||
## 🌟 Real-World Benefits
|
||||
## 🏗️ Architecture
|
||||
|
||||
### For Individual Developers
|
||||
- **Faster Problem Solving**: Find solutions you've used before instantly
|
||||
- **Knowledge Accumulation**: Build expertise that persists across projects
|
||||
- **Context Continuity**: Pick up where you left off, even weeks later
|
||||
- **Pattern Recognition**: See how you've solved similar problems before
|
||||
**Storage Layers**
|
||||
- **ChromaDB**: Vector database for semantic search with embeddings
|
||||
- **SQLite**: Metadata index (`~/.claude-mem/claude-mem.db`) with sessions, memories, overviews
|
||||
- **Archives**: Compressed transcript backups in `~/.claude-mem/archives/`
|
||||
|
||||
### For Teams (Coming Soon)
|
||||
- **Shared Knowledge**: Team-wide memory accessible to all members
|
||||
- **Onboarding Acceleration**: New team members access collective knowledge
|
||||
- **Best Practices**: Capture and share proven solutions
|
||||
- **Institutional Memory**: Prevent knowledge loss when team members leave
|
||||
**Hook System** (`hook-templates/`)
|
||||
- `user-prompt-submit.js`: Captures user prompts immediately, stores in ChromaDB
|
||||
- `post-tool-use.js`: Spawns Agent SDK for async compression of tool responses
|
||||
- `stop.js`: Generates session overview, cleans up SDK transcripts from UI
|
||||
- `session-start.js`: Loads relevant context on startup and `/clear`
|
||||
- Shared utilities: `hook-helpers.js`, `hook-prompt-renderer.js`, `config-loader.js`, `path-resolver.js`
|
||||
|
||||
## 🚀 Coming Soon: Cloud Sync
|
||||
**CLI Commands** (`src/commands/`)
|
||||
- Installation, status, and diagnostics
|
||||
- Memory storage and retrieval
|
||||
- Changelog generation from memories
|
||||
- Smart Trash™ management
|
||||
- 15+ dynamic ChromaDB MCP tool wrappers
|
||||
|
||||
### Individual Plan ($9.95/month)
|
||||
- **Multi-device sync**: Access your memories on any device
|
||||
- **Cloud backup**: Never lose your knowledge base
|
||||
- **Enhanced search**: Advanced filtering and semantic search
|
||||
- **API access**: Integrate with your own tools and workflows
|
||||
**Services** (`src/services/`)
|
||||
- SQLite stores: Session, Memory, Overview, Diagnostics, TranscriptEvent
|
||||
- Path discovery for project detection
|
||||
- Rolling settings and logs
|
||||
|
||||
### Team Plan ($29.95/month, 3+ seats)
|
||||
- **Shared memories**: Team-wide knowledge base
|
||||
- **Role-based access**: Control what memories are shared
|
||||
- **Admin dashboard**: Manage team members and usage
|
||||
- **Priority support**: Direct access to our engineering team
|
||||
## 🔍 How Memory Search Works
|
||||
|
||||
[**Join the waitlist**](https://claude-mem.ai) for early access to cloud features.
|
||||
**Semantic Search Best Practices**:
|
||||
```typescript
|
||||
// ALWAYS include project name to avoid cross-contamination
|
||||
mcp__claude-mem__chroma_query_documents({
|
||||
collection_name: "claude_memories",
|
||||
query_texts: ["claude-mem authentication bug"],
|
||||
n_results: 10
|
||||
})
|
||||
|
||||
## 🛡️ Privacy & Security
|
||||
// Include dates for temporal search (dates in query text, not metadata)
|
||||
mcp__claude-mem__chroma_query_documents({
|
||||
collection_name: "claude_memories",
|
||||
query_texts: ["project-name 2025-10-02 feature implementation"],
|
||||
n_results: 5
|
||||
})
|
||||
|
||||
- **Local-first**: All data stored locally by default
|
||||
- **No tracking**: We don't collect or transmit your conversations
|
||||
- **Your data**: You own and control your knowledge base
|
||||
- **Open architecture**: ChromaDB and MCP are open standards
|
||||
// Intent-based queries work better than keyword matching
|
||||
mcp__claude-mem__chroma_query_documents({
|
||||
collection_name: "claude_memories",
|
||||
query_texts: ["implementing oauth flow"],
|
||||
n_results: 10
|
||||
})
|
||||
```
|
||||
|
||||
**What Doesn't Work** (Avoid These!)
|
||||
- ❌ Complex `where` filters with `$and`/`$or` - causes errors
|
||||
- ❌ Timestamp comparisons (`$gte`, `$lt`) - stored as strings
|
||||
- ❌ Mixing project filters in where clause - causes "Error finding id"
|
||||
|
||||
**Storage Collection**: `claude_memories`
|
||||
- Metadata: `project`, `session_id`, `date`, `type`, `concepts`, `files`
|
||||
- Embeddings: Semantic vectors for similarity search
|
||||
- Documents: Atomic facts + full narrative with hierarchical structure
|
||||
|
||||
## ✅ Requirements
|
||||
|
||||
- Node.js >= 18.0.0
|
||||
- Bun >= 1.0.0 (for development)
|
||||
- Claude Code with MCP support
|
||||
- macOS/Linux (POSIX-compliant)
|
||||
|
||||
## 🛠️ Development
|
||||
|
||||
```bash
|
||||
# Development mode
|
||||
bun run dev
|
||||
|
||||
# Build production bundle
|
||||
bun run build
|
||||
|
||||
# Build and update hooks (RECOMMENDED for hook changes)
|
||||
bun run build && bun link && claude-mem install --force
|
||||
|
||||
# Run tests
|
||||
bun test # All tests
|
||||
npm run test:integration # Integration tests
|
||||
bun run test:unit # Unit tests only
|
||||
|
||||
# Install from source
|
||||
bun run dev:install
|
||||
|
||||
# Live Memory Viewer
|
||||
npm run memory-stream:server # Start SSE server on :3001
|
||||
|
||||
# Code quality
|
||||
bun run lint
|
||||
bun run format
|
||||
```
|
||||
|
||||
## 🎨 Live Memory Viewer
|
||||
|
||||
Real-time slideshow of memories with SSE streaming:
|
||||
|
||||
1. Start the server: `npm run memory-stream:server`
|
||||
2. Open the viewer at `src/ui/memory-stream/`
|
||||
3. Auto-connects to `~/.claude-mem/claude-mem.db`
|
||||
4. New memories appear instantly as they're created
|
||||
|
||||
Features:
|
||||
- 📡 Live SSE streaming from SQLite WAL changes
|
||||
- 🎬 Auto-slideshow (5s intervals)
|
||||
- ⏸️ Pause/Resume with Space bar
|
||||
- ⌨️ Keyboard navigation (←/→)
|
||||
- 🎨 Cyberpunk neural network aesthetic
|
||||
|
||||
## 🔑 Key Design Decisions
|
||||
|
||||
**Storage Architecture**
|
||||
- Direct ChromaDB writes in `store-memory.ts` command (no async syncing)
|
||||
- Each atomic fact stored as separate document + full narrative document
|
||||
- Hierarchical metadata: project, session, date, type, concepts, files
|
||||
- SQLite for fast metadata queries, ChromaDB for semantic search
|
||||
|
||||
**Hook Infrastructure**
|
||||
- Streaming hooks (<50ms overhead) capture real-time events
|
||||
- Shared utilities in `hook-templates/shared/` for consistency
|
||||
- Force overwrite on install to ensure latest hook code deploys
|
||||
- Milliseconds in `config.json`, seconds in Claude settings
|
||||
|
||||
**Memory Compression**
|
||||
- Agent SDK spawned asynchronously for tool response compression
|
||||
- User prompts stored immediately without blocking
|
||||
- SDK transcripts auto-deleted to keep UI clean
|
||||
- 100:1 compression ratio maintained
|
||||
|
||||
**Search Strategy**
|
||||
- Semantic search via query text (dates embedded in queries)
|
||||
- Avoid complex metadata filters (causes ChromaDB errors)
|
||||
- Always include project name in queries for isolation
|
||||
- Multiple query phrasings for better coverage
|
||||
|
||||
## 🆘 Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
**Hook not triggering?**
|
||||
```bash
|
||||
claude-mem status # Check installation
|
||||
claude-mem install --force # Reinstall hooks
|
||||
claude-mem status # Check installation health
|
||||
claude-mem doctor # Run full diagnostics
|
||||
claude-mem install --force # Repair installation
|
||||
claude-mem logs # View recent operations
|
||||
```
|
||||
|
||||
**Context not loading?**
|
||||
```bash
|
||||
claude-mem load-context # Verify memories exist
|
||||
claude-mem logs # Check for errors
|
||||
```
|
||||
|
||||
**Performance issues?**
|
||||
```bash
|
||||
# ChromaDB maintenance (if needed)
|
||||
claude-mem status # Check memory usage
|
||||
```
|
||||
|
||||
## 🔧 Requirements
|
||||
|
||||
- **Node.js**: 18.0 or higher
|
||||
- **Claude Code**: Latest version recommended
|
||||
- **Storage**: ~100MB for typical usage
|
||||
- **Memory**: 2GB RAM minimum for large knowledge bases
|
||||
|
||||
## 📞 Support & Community
|
||||
|
||||
- **Documentation**: Complete guides at [claude-mem.ai/docs](https://claude-mem.ai/docs)
|
||||
- **Issues**: Report bugs at [GitHub Issues](https://github.com/thedotmack/claude-mem/issues)
|
||||
- **Feature Requests**: [GitHub Discussions](https://github.com/thedotmack/claude-mem/discussions)
|
||||
- **Community**: Join our [Discord](https://discord.gg/claude-mem) for tips and discussions
|
||||
|
||||
## 📄 License
|
||||
|
||||
This software is free to use but is NOT open source. See [LICENSE](LICENSE) file for complete terms.
|
||||
AGPL-3.0 - See LICENSE file for details
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Ready to Transform Your Claude Code Experience?
|
||||
|
||||
```bash
|
||||
npm install -g claude-mem
|
||||
claude-mem install
|
||||
```
|
||||
|
||||
**Your AI assistant is about to get a lot smarter.** 🧠✨
|
||||
|
||||
---
|
||||
|
||||
*Built with ❤️ for developers who believe AI assistants should remember and learn from every conversation.*
|
||||
**Remember more. Repeat less.** 🧠✨
|
||||
-40
@@ -1,40 +0,0 @@
|
||||
# Claude Mem v3.3.8
|
||||
|
||||
## 🚀 Installation
|
||||
|
||||
### Quick Install (Recommended)
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/thedotmack/claude-mem/main/install.sh | bash
|
||||
```
|
||||
|
||||
### Manual Download
|
||||
Download the appropriate binary for your platform from the [releases page](https://github.com/thedotmack/claude-mem/releases/latest):
|
||||
|
||||
- **Windows x64**: `claude-mem.exe`
|
||||
- **Linux x64**: `claude-mem-linux`
|
||||
- **Linux ARM64**: `claude-mem-linux-arm64`
|
||||
- **macOS ARM64** (Apple Silicon): `claude-mem-macos-arm64`
|
||||
- **macOS x64** (Intel): `claude-mem-macos-x64`
|
||||
|
||||
## 📦 What's Included
|
||||
|
||||
- Multi-platform binaries for Windows, Linux, and macOS
|
||||
- Hook system for customization
|
||||
- Full documentation
|
||||
- MCP server integration for Claude Code
|
||||
|
||||
## 🔧 Supported Platforms
|
||||
|
||||
| Platform | Architecture | Binary Name |
|
||||
|----------|--------------|-------------|
|
||||
| Windows | x64 | claude-mem.exe |
|
||||
| Linux | x64 | claude-mem-linux |
|
||||
| Linux | ARM64 | claude-mem-linux-arm64 |
|
||||
| macOS | ARM64 (Apple Silicon) | claude-mem-macos-arm64 |
|
||||
| macOS | x64 (Intel) | claude-mem-macos-x64 |
|
||||
|
||||
## 📝 License
|
||||
|
||||
Binary distribution under proprietary license (free to use).
|
||||
Hook files under MIT license (open source).
|
||||
See LICENSE file for details.
|
||||
@@ -0,0 +1,23 @@
|
||||
---
|
||||
argument-hint: help | save [message] | remember [context] | (no args for help)
|
||||
description: Manage claude-mem operations and memory context
|
||||
allowed-tools: Bash(claude-mem:*), Bash(echo:*), Bash(cat:*)
|
||||
---
|
||||
|
||||
## Claude-Mem Command Handler
|
||||
|
||||
### Check for help command first
|
||||
!`[ -z "$ARGUMENTS" ] || [ "$ARGUMENTS" = "help" ] && printf '%s\n' '## 🧠 Claude-Mem Help' '' '**Available Commands:**' '' '• /claude-mem save [message] - Quick save of conversation overview' '• /claude-mem remember [query] - Search saved memories' '• /claude-mem help - Show this help' '' '**Quick Shortcuts:**' '• /save - Direct save' '• /remember - Direct search' '' '**About /save:**' 'Quick way to save an overview to claude-mem without processing the' 'entire transcript. Use this when you dont need a detailed archive,' 'just a summary of key points and decisions.' '' '**Optional Features (configure during install):**' '• Compress on /clear: Archives full transcript when clearing (off by default)' '• Session start: Loads recent memories when starting Claude Code' '' 'For more details: claude-mem --help' && exit 0`
|
||||
|
||||
### Process other commands
|
||||
Handle claude-mem operation: $ARGUMENTS
|
||||
|
||||
If $ARGUMENTS starts with "save":
|
||||
- Write an overview of the current conversation context
|
||||
- Add it to claude-mem using the chroma MCP tools
|
||||
- Save the overview using: `claude-mem save "your overview message"`
|
||||
|
||||
If $ARGUMENTS starts with "remember":
|
||||
- Search claude-mem for relevant memories using the query
|
||||
- Display the most relevant memories from previous sessions
|
||||
- Use chroma_query_documents to find and present context
|
||||
@@ -0,0 +1 @@
|
||||
Search claude-mem for #$ARGUMENTS and look up relevant context to help clarify what we are working on.
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
allowed-tools: Bash
|
||||
description: Write an overview and save with claude-mem
|
||||
---
|
||||
**Write an overview** of the current conversation context and:
|
||||
1. **Add it to claude-mem** using the chroma MCP tools. Always use primitive types (strings, numbers, booleans) when calling MCP Chroma tools directly. Arrays should be comma-separated strings, and nested objects should be flattened.
|
||||
2. **Save the overview to index** using the claude-mem CLI tool: `claude-mem save "your overview message"`
|
||||
+662
File diff suppressed because one or more lines are too long
@@ -1,96 +0,0 @@
|
||||
# Multi-Platform Build Guide
|
||||
|
||||
This project now supports building binaries for multiple platforms using Bun's cross-compilation capabilities.
|
||||
|
||||
## Supported Platforms
|
||||
|
||||
- **Windows x64**: `claude-mem.exe`
|
||||
- **Linux x64**: `claude-mem-linux`
|
||||
- **Linux ARM64**: `claude-mem-linux-arm64`
|
||||
- **macOS ARM64**: `claude-mem-macos-arm64`
|
||||
- **macOS x64**: `claude-mem-macos-x64`
|
||||
|
||||
## Building
|
||||
|
||||
### Build All Platforms
|
||||
|
||||
To build binaries for all supported platforms:
|
||||
|
||||
```bash
|
||||
npm run build:multiplatform
|
||||
```
|
||||
|
||||
This will create binaries in the `releases/binaries/` directory.
|
||||
|
||||
### Build for NPM Package
|
||||
|
||||
To build a complete npm package with all platform binaries:
|
||||
|
||||
```bash
|
||||
npm run publish
|
||||
```
|
||||
|
||||
This creates a package in `releases/npm-package/` that includes:
|
||||
- Platform detection wrapper script
|
||||
- All platform-specific binaries
|
||||
- Hooks and configuration files
|
||||
|
||||
## How Platform Detection Works
|
||||
|
||||
The npm package includes a Node.js wrapper script (`claude-mem`) that:
|
||||
|
||||
1. Detects the current platform using `process.platform` and `process.arch`
|
||||
2. Maps the platform to the appropriate binary filename
|
||||
3. Executes the correct binary with all command-line arguments
|
||||
|
||||
### Platform Mapping
|
||||
|
||||
| Platform | Architecture | Binary Filename |
|
||||
|----------|-------------|------------------|
|
||||
| Windows | x64 | `claude-mem.exe` |
|
||||
| Linux | x64 | `claude-mem-linux` |
|
||||
| Linux | arm64/aarch64 | `claude-mem-linux-arm64` |
|
||||
| macOS | arm64 | `claude-mem-macos-arm64` |
|
||||
| macOS | x64 | `claude-mem-macos-x64` |
|
||||
|
||||
## Usage
|
||||
|
||||
After installation via npm, users can run:
|
||||
|
||||
```bash
|
||||
npx claude-mem --help
|
||||
```
|
||||
|
||||
The wrapper will automatically select and execute the correct binary for their platform.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Unsupported Platform Error
|
||||
|
||||
If you see an "Unsupported platform" error, check that your platform/architecture combination is in the supported list above.
|
||||
|
||||
### Binary Not Found Error
|
||||
|
||||
This indicates the platform detection worked, but the expected binary file is missing from the package. This shouldn't happen with properly built packages.
|
||||
|
||||
## Development
|
||||
|
||||
### Adding New Platforms
|
||||
|
||||
To add support for new platforms:
|
||||
|
||||
1. Add the platform to the `PLATFORMS` array in `scripts/build-multiplatform.sh`
|
||||
2. Update the platform detection logic in `scripts/claude-mem-wrapper.js`
|
||||
3. Update this documentation
|
||||
|
||||
### Testing Binaries
|
||||
|
||||
Test that a specific binary works:
|
||||
|
||||
```bash
|
||||
# Test Linux binary
|
||||
./releases/binaries/claude-mem-linux --help
|
||||
|
||||
# Test Windows binary (on Windows or with Wine)
|
||||
./releases/binaries/claude-mem.exe --help
|
||||
```
|
||||
@@ -1,584 +0,0 @@
|
||||
Bun's bundler implements a `--compile` flag for generating a standalone binary from a TypeScript or JavaScript file.
|
||||
|
||||
{% codetabs %}
|
||||
|
||||
```bash
|
||||
$ bun build ./cli.ts --compile --outfile mycli
|
||||
```
|
||||
|
||||
```ts#cli.ts
|
||||
console.log("Hello world!");
|
||||
```
|
||||
|
||||
{% /codetabs %}
|
||||
|
||||
This bundles `cli.ts` into an executable that can be executed directly:
|
||||
|
||||
```
|
||||
$ ./mycli
|
||||
Hello world!
|
||||
```
|
||||
|
||||
All imported files and packages are bundled into the executable, along with a copy of the Bun runtime. All built-in Bun and Node.js APIs are supported.
|
||||
|
||||
## Cross-compile to other platforms
|
||||
|
||||
The `--target` flag lets you compile your standalone executable for a different operating system, architecture, or version of Bun than the machine you're running `bun build` on.
|
||||
|
||||
To build for Linux x64 (most servers):
|
||||
|
||||
```sh
|
||||
bun build --compile --target=bun-linux-x64 ./index.ts --outfile myapp
|
||||
|
||||
# To support CPUs from before 2013, use the baseline version (nehalem)
|
||||
bun build --compile --target=bun-linux-x64-baseline ./index.ts --outfile myapp
|
||||
|
||||
# To explicitly only support CPUs from 2013 and later, use the modern version (haswell)
|
||||
# modern is faster, but baseline is more compatible.
|
||||
bun build --compile --target=bun-linux-x64-modern ./index.ts --outfile myapp
|
||||
```
|
||||
|
||||
To build for Linux ARM64 (e.g. Graviton or Raspberry Pi):
|
||||
|
||||
```sh
|
||||
# Note: the default architecture is x64 if no architecture is specified.
|
||||
bun build --compile --target=bun-linux-arm64 ./index.ts --outfile myapp
|
||||
```
|
||||
|
||||
To build for Windows x64:
|
||||
|
||||
```sh
|
||||
bun build --compile --target=bun-windows-x64 ./path/to/my/app.ts --outfile myapp
|
||||
|
||||
# To support CPUs from before 2013, use the baseline version (nehalem)
|
||||
bun build --compile --target=bun-windows-x64-baseline ./path/to/my/app.ts --outfile myapp
|
||||
|
||||
# To explicitly only support CPUs from 2013 and later, use the modern version (haswell)
|
||||
bun build --compile --target=bun-windows-x64-modern ./path/to/my/app.ts --outfile myapp
|
||||
|
||||
# note: if no .exe extension is provided, Bun will automatically add it for Windows executables
|
||||
```
|
||||
|
||||
To build for macOS arm64:
|
||||
|
||||
```sh
|
||||
bun build --compile --target=bun-darwin-arm64 ./path/to/my/app.ts --outfile myapp
|
||||
```
|
||||
|
||||
To build for macOS x64:
|
||||
|
||||
```sh
|
||||
bun build --compile --target=bun-darwin-x64 ./path/to/my/app.ts --outfile myapp
|
||||
```
|
||||
|
||||
#### Supported targets
|
||||
|
||||
The order of the `--target` flag does not matter, as long as they're delimited by a `-`.
|
||||
|
||||
| --target | Operating System | Architecture | Modern | Baseline | Libc |
|
||||
| --------------------- | ---------------- | ------------ | ------ | -------- | ----- |
|
||||
| bun-linux-x64 | Linux | x64 | ✅ | ✅ | glibc |
|
||||
| bun-linux-arm64 | Linux | arm64 | ✅ | N/A | glibc |
|
||||
| bun-windows-x64 | Windows | x64 | ✅ | ✅ | - |
|
||||
| ~~bun-windows-arm64~~ | Windows | arm64 | ❌ | ❌ | - |
|
||||
| bun-darwin-x64 | macOS | x64 | ✅ | ✅ | - |
|
||||
| bun-darwin-arm64 | macOS | arm64 | ✅ | N/A | - |
|
||||
| bun-linux-x64-musl | Linux | x64 | ✅ | ✅ | musl |
|
||||
| bun-linux-arm64-musl | Linux | arm64 | ✅ | N/A | musl |
|
||||
|
||||
On x64 platforms, Bun uses SIMD optimizations which require a modern CPU supporting AVX2 instructions. The `-baseline` build of Bun is for older CPUs that don't support these optimizations. Normally, when you install Bun we automatically detect which version to use but this can be harder to do when cross-compiling since you might not know the target CPU. You usually don't need to worry about it on Darwin x64, but it is relevant for Windows x64 and Linux x64. If you or your users see `"Illegal instruction"` errors, you might need to use the baseline version.
|
||||
|
||||
## Build-time constants
|
||||
|
||||
Use the `--define` flag to inject build-time constants into your executable, such as version numbers, build timestamps, or configuration values:
|
||||
|
||||
```bash
|
||||
$ bun build --compile --define BUILD_VERSION='"1.2.3"' --define BUILD_TIME='"2024-01-15T10:30:00Z"' src/cli.ts --outfile mycli
|
||||
```
|
||||
|
||||
These constants are embedded directly into your compiled binary at build time, providing zero runtime overhead and enabling dead code elimination optimizations.
|
||||
|
||||
{% callout type="info" %}
|
||||
For comprehensive examples and advanced patterns, see the [Build-time constants guide](/guides/runtime/build-time-constants).
|
||||
{% /callout %}
|
||||
|
||||
## Deploying to production
|
||||
|
||||
Compiled executables reduce memory usage and improve Bun's start time.
|
||||
|
||||
Normally, Bun reads and transpiles JavaScript and TypeScript files on `import` and `require`. This is part of what makes so much of Bun "just work", but it's not free. It costs time and memory to read files from disk, resolve file paths, parse, transpile, and print source code.
|
||||
|
||||
With compiled executables, you can move that cost from runtime to build-time.
|
||||
|
||||
When deploying to production, we recommend the following:
|
||||
|
||||
```sh
|
||||
bun build --compile --minify --sourcemap ./path/to/my/app.ts --outfile myapp
|
||||
```
|
||||
|
||||
### Bytecode compilation
|
||||
|
||||
To improve startup time, enable bytecode compilation:
|
||||
|
||||
```sh
|
||||
bun build --compile --minify --sourcemap --bytecode ./path/to/my/app.ts --outfile myapp
|
||||
```
|
||||
|
||||
Using bytecode compilation, `tsc` starts 2x faster:
|
||||
|
||||
{% image src="https://github.com/user-attachments/assets/dc8913db-01d2-48f8-a8ef-ac4e984f9763" width="689" /%}
|
||||
|
||||
Bytecode compilation moves parsing overhead for large input files from runtime to bundle time. Your app starts faster, in exchange for making the `bun build` command a little slower. It doesn't obscure source code.
|
||||
|
||||
**Experimental:** Bytecode compilation is an experimental feature introduced in Bun v1.1.30. Only `cjs` format is supported (which means no top-level-await). Let us know if you run into any issues!
|
||||
|
||||
### What do these flags do?
|
||||
|
||||
The `--minify` argument optimizes the size of the transpiled output code. If you have a large application, this can save megabytes of space. For smaller applications, it might still improve start time a little.
|
||||
|
||||
The `--sourcemap` argument embeds a sourcemap compressed with zstd, so that errors & stacktraces point to their original locations instead of the transpiled location. Bun will automatically decompress & resolve the sourcemap when an error occurs.
|
||||
|
||||
The `--bytecode` argument enables bytecode compilation. Every time you run JavaScript code in Bun, JavaScriptCore (the engine) will compile your source code into bytecode. We can move this parsing work from runtime to bundle time, saving you startup time.
|
||||
|
||||
## Act as the Bun CLI
|
||||
|
||||
{% note %}
|
||||
|
||||
New in Bun v1.2.16
|
||||
|
||||
{% /note %}
|
||||
|
||||
You can run a standalone executable as if it were the `bun` CLI itself by setting the `BUN_BE_BUN=1` environment variable. When this variable is set, the executable will ignore its bundled entrypoint and instead expose all the features of Bun's CLI.
|
||||
|
||||
For example, consider an executable compiled from a simple script:
|
||||
|
||||
```sh
|
||||
$ cat such-bun.js
|
||||
console.log("you shouldn't see this");
|
||||
|
||||
$ bun build --compile ./such-bun.js
|
||||
[3ms] bundle 1 modules
|
||||
[89ms] compile such-bun
|
||||
```
|
||||
|
||||
Normally, running `./such-bun` with arguments would execute the script. However, with the `BUN_BE_BUN=1` environment variable, it acts just like the `bun` binary:
|
||||
|
||||
```sh
|
||||
# Executable runs its own entrypoint by default
|
||||
$ ./such-bun install
|
||||
you shouldn't see this
|
||||
|
||||
# With the env var, the executable acts like the `bun` CLI
|
||||
$ BUN_BE_BUN=1 ./such-bun install
|
||||
bun install v1.2.16-canary.1 (1d1db811)
|
||||
Checked 63 installs across 64 packages (no changes) [5.00ms]
|
||||
```
|
||||
|
||||
This is useful for building CLI tools on top of Bun that may need to install packages, bundle dependencies, run different or local files and more without needing to download a separate binary or install bun.
|
||||
|
||||
## Full-stack executables
|
||||
|
||||
{% note %}
|
||||
|
||||
New in Bun v1.2.17
|
||||
|
||||
{% /note %}
|
||||
|
||||
Bun's `--compile` flag can create standalone executables that contain both server and client code, making it ideal for full-stack applications. When you import an HTML file in your server code, Bun automatically bundles all frontend assets (JavaScript, CSS, etc.) and embeds them into the executable. When Bun sees the HTML import on the server, it kicks off a frontend build process to bundle JavaScript, CSS, and other assets.
|
||||
|
||||
{% codetabs %}
|
||||
|
||||
```ts#server.ts
|
||||
import { serve } from "bun";
|
||||
import index from "./index.html";
|
||||
|
||||
const server = serve({
|
||||
routes: {
|
||||
"/": index,
|
||||
"/api/hello": { GET: () => Response.json({ message: "Hello from API" }) },
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`Server running at http://localhost:${server.port}`);
|
||||
```
|
||||
|
||||
```html#index.html
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>My App</title>
|
||||
<link rel="stylesheet" href="./styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hello World</h1>
|
||||
<script src="./app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
```js#app.js
|
||||
console.log("Hello from the client!");
|
||||
```
|
||||
|
||||
```css#styles.css
|
||||
body {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
```
|
||||
|
||||
{% /codetabs %}
|
||||
|
||||
To build this into a single executable:
|
||||
|
||||
```sh
|
||||
bun build --compile ./server.ts --outfile myapp
|
||||
```
|
||||
|
||||
This creates a self-contained binary that includes:
|
||||
|
||||
- Your server code
|
||||
- The Bun runtime
|
||||
- All frontend assets (HTML, CSS, JavaScript)
|
||||
- Any npm packages used by your server
|
||||
|
||||
The result is a single file that can be deployed anywhere without needing Node.js, Bun, or any dependencies installed. Just run:
|
||||
|
||||
```sh
|
||||
./myapp
|
||||
```
|
||||
|
||||
Bun automatically handles serving the frontend assets with proper MIME types and cache headers. The HTML import is replaced with a manifest object that `Bun.serve` uses to efficiently serve pre-bundled assets.
|
||||
|
||||
For more details on building full-stack applications with Bun, see the [full-stack guide](/docs/bundler/fullstack).
|
||||
|
||||
## Worker
|
||||
|
||||
To use workers in a standalone executable, add the worker's entrypoint to the CLI arguments:
|
||||
|
||||
```sh
|
||||
$ bun build --compile ./index.ts ./my-worker.ts --outfile myapp
|
||||
```
|
||||
|
||||
Then, reference the worker in your code:
|
||||
|
||||
```ts
|
||||
console.log("Hello from Bun!");
|
||||
|
||||
// Any of these will work:
|
||||
new Worker("./my-worker.ts");
|
||||
new Worker(new URL("./my-worker.ts", import.meta.url));
|
||||
new Worker(new URL("./my-worker.ts", import.meta.url).href);
|
||||
```
|
||||
|
||||
As of Bun v1.1.25, when you add multiple entrypoints to a standalone executable, they will be bundled separately into the executable.
|
||||
|
||||
In the future, we may automatically detect usages of statically-known paths in `new Worker(path)` and then bundle those into the executable, but for now, you'll need to add it to the shell command manually like the above example.
|
||||
|
||||
If you use a relative path to a file not included in the standalone executable, it will attempt to load that path from disk relative to the current working directory of the process (and then error if it doesn't exist).
|
||||
|
||||
## SQLite
|
||||
|
||||
You can use `bun:sqlite` imports with `bun build --compile`.
|
||||
|
||||
By default, the database is resolved relative to the current working directory of the process.
|
||||
|
||||
```js
|
||||
import db from "./my.db" with { type: "sqlite" };
|
||||
|
||||
console.log(db.query("select * from users LIMIT 1").get());
|
||||
```
|
||||
|
||||
That means if the executable is located at `/usr/bin/hello`, the user's terminal is located at `/home/me/Desktop`, it will look for `/home/me/Desktop/my.db`.
|
||||
|
||||
```
|
||||
$ cd /home/me/Desktop
|
||||
$ ./hello
|
||||
```
|
||||
|
||||
## Embed assets & files
|
||||
|
||||
Standalone executables support embedding files.
|
||||
|
||||
To embed files into an executable with `bun build --compile`, import the file in your code.
|
||||
|
||||
```ts
|
||||
// this becomes an internal file path
|
||||
import icon from "./icon.png" with { type: "file" };
|
||||
import { file } from "bun";
|
||||
|
||||
export default {
|
||||
fetch(req) {
|
||||
// Embedded files can be streamed from Response objects
|
||||
return new Response(file(icon));
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
Embedded files can be read using `Bun.file`'s functions or the Node.js `fs.readFile` function (in `"node:fs"`).
|
||||
|
||||
For example, to read the contents of the embedded file:
|
||||
|
||||
```js
|
||||
import icon from "./icon.png" with { type: "file" };
|
||||
import { file } from "bun";
|
||||
|
||||
const bytes = await file(icon).arrayBuffer();
|
||||
// await fs.promises.readFile(icon)
|
||||
// fs.readFileSync(icon)
|
||||
```
|
||||
|
||||
### Embed SQLite databases
|
||||
|
||||
If your application wants to embed a SQLite database, set `type: "sqlite"` in the import attribute and the `embed` attribute to `"true"`.
|
||||
|
||||
```js
|
||||
import myEmbeddedDb from "./my.db" with { type: "sqlite", embed: "true" };
|
||||
|
||||
console.log(myEmbeddedDb.query("select * from users LIMIT 1").get());
|
||||
```
|
||||
|
||||
This database is read-write, but all changes are lost when the executable exits (since it's stored in memory).
|
||||
|
||||
### Embed N-API Addons
|
||||
|
||||
As of Bun v1.0.23, you can embed `.node` files into executables.
|
||||
|
||||
```js
|
||||
const addon = require("./addon.node");
|
||||
|
||||
console.log(addon.hello());
|
||||
```
|
||||
|
||||
Unfortunately, if you're using `@mapbox/node-pre-gyp` or other similar tools, you'll need to make sure the `.node` file is directly required or it won't bundle correctly.
|
||||
|
||||
### Embed directories
|
||||
|
||||
To embed a directory with `bun build --compile`, use a shell glob in your `bun build` command:
|
||||
|
||||
```sh
|
||||
$ bun build --compile ./index.ts ./public/**/*.png
|
||||
```
|
||||
|
||||
Then, you can reference the files in your code:
|
||||
|
||||
```ts
|
||||
import icon from "./public/assets/icon.png" with { type: "file" };
|
||||
import { file } from "bun";
|
||||
|
||||
export default {
|
||||
fetch(req) {
|
||||
// Embedded files can be streamed from Response objects
|
||||
return new Response(file(icon));
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
This is honestly a workaround, and we expect to improve this in the future with a more direct API.
|
||||
|
||||
### Listing embedded files
|
||||
|
||||
To get a list of all embedded files, use `Bun.embeddedFiles`:
|
||||
|
||||
```js
|
||||
import "./icon.png" with { type: "file" };
|
||||
import { embeddedFiles } from "bun";
|
||||
|
||||
console.log(embeddedFiles[0].name); // `icon-${hash}.png`
|
||||
```
|
||||
|
||||
`Bun.embeddedFiles` returns an array of `Blob` objects which you can use to get the size, contents, and other properties of the files.
|
||||
|
||||
```ts
|
||||
embeddedFiles: Blob[]
|
||||
```
|
||||
|
||||
The list of embedded files excludes bundled source code like `.ts` and `.js` files.
|
||||
|
||||
#### Content hash
|
||||
|
||||
By default, embedded files have a content hash appended to their name. This is useful for situations where you want to serve the file from a URL or CDN and have fewer cache invalidation issues. But sometimes, this is unexpected and you might want the original name instead:
|
||||
|
||||
To disable the content hash, pass `--asset-naming` to `bun build --compile` like this:
|
||||
|
||||
```sh
|
||||
$ bun build --compile --asset-naming="[name].[ext]" ./index.ts
|
||||
```
|
||||
|
||||
## Minification
|
||||
|
||||
To trim down the size of the executable a little, pass `--minify` to `bun build --compile`. This uses Bun's minifier to reduce the code size. Overall though, Bun's binary is still way too big and we need to make it smaller.
|
||||
|
||||
## Using Bun.build() API
|
||||
|
||||
You can also generate standalone executables using the `Bun.build()` JavaScript API. This is useful when you need programmatic control over the build process.
|
||||
|
||||
### Basic usage
|
||||
|
||||
```js
|
||||
await Bun.build({
|
||||
entrypoints: ["./app.ts"],
|
||||
outdir: "./dist",
|
||||
compile: {
|
||||
target: "bun-windows-x64",
|
||||
outfile: "myapp.exe",
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Windows metadata with Bun.build()
|
||||
|
||||
When targeting Windows, you can specify metadata through the `windows` object:
|
||||
|
||||
```js
|
||||
await Bun.build({
|
||||
entrypoints: ["./app.ts"],
|
||||
outdir: "./dist",
|
||||
compile: {
|
||||
target: "bun-windows-x64",
|
||||
outfile: "myapp.exe",
|
||||
windows: {
|
||||
title: "My Application",
|
||||
publisher: "My Company Inc",
|
||||
version: "1.2.3.4",
|
||||
description: "A powerful application built with Bun",
|
||||
copyright: "© 2024 My Company Inc",
|
||||
hideConsole: false, // Set to true for GUI applications
|
||||
icon: "./icon.ico", // Path to icon file
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Cross-compilation with Bun.build()
|
||||
|
||||
You can cross-compile for different platforms:
|
||||
|
||||
```js
|
||||
// Build for multiple platforms
|
||||
const platforms = [
|
||||
{ target: "bun-windows-x64", outfile: "app-windows.exe" },
|
||||
{ target: "bun-linux-x64", outfile: "app-linux" },
|
||||
{ target: "bun-darwin-arm64", outfile: "app-macos" },
|
||||
];
|
||||
|
||||
for (const platform of platforms) {
|
||||
await Bun.build({
|
||||
entrypoints: ["./app.ts"],
|
||||
outdir: "./dist",
|
||||
compile: platform,
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## Windows-specific flags
|
||||
|
||||
When compiling a standalone executable for Windows, there are several platform-specific options that can be used to customize the generated `.exe` file:
|
||||
|
||||
### Visual customization
|
||||
|
||||
- `--windows-icon=path/to/icon.ico` - Set the executable file icon
|
||||
- `--windows-hide-console` - Disable the background terminal window (useful for GUI applications)
|
||||
|
||||
### Metadata customization
|
||||
|
||||
You can embed version information and other metadata into your Windows executable:
|
||||
|
||||
- `--windows-title <STR>` - Set the product name (appears in file properties)
|
||||
- `--windows-publisher <STR>` - Set the company name
|
||||
- `--windows-version <STR>` - Set the version number (e.g. "1.2.3.4")
|
||||
- `--windows-description <STR>` - Set the file description
|
||||
- `--windows-copyright <STR>` - Set the copyright information
|
||||
|
||||
#### Example with all metadata flags:
|
||||
|
||||
```sh
|
||||
bun build --compile ./app.ts \
|
||||
--outfile myapp.exe \
|
||||
--windows-title "My Application" \
|
||||
--windows-publisher "My Company Inc" \
|
||||
--windows-version "1.2.3.4" \
|
||||
--windows-description "A powerful application built with Bun" \
|
||||
--windows-copyright "© 2024 My Company Inc"
|
||||
```
|
||||
|
||||
This metadata will be visible in Windows Explorer when viewing the file properties:
|
||||
|
||||
1. Right-click the executable in Windows Explorer
|
||||
2. Select "Properties"
|
||||
3. Go to the "Details" tab
|
||||
|
||||
#### Version string format
|
||||
|
||||
The `--windows-version` flag accepts version strings in the following formats:
|
||||
|
||||
- `"1"` - Will be normalized to "1.0.0.0"
|
||||
- `"1.2"` - Will be normalized to "1.2.0.0"
|
||||
- `"1.2.3"` - Will be normalized to "1.2.3.0"
|
||||
- `"1.2.3.4"` - Full version format
|
||||
|
||||
Each version component must be a number between 0 and 65535.
|
||||
|
||||
{% callout %}
|
||||
|
||||
These flags currently cannot be used when cross-compiling because they depend on Windows APIs. They are only available when building on Windows itself.
|
||||
|
||||
{% /callout %}
|
||||
|
||||
## Code signing on macOS
|
||||
|
||||
To codesign a standalone executable on macOS (which fixes Gatekeeper warnings), use the `codesign` command.
|
||||
|
||||
```sh
|
||||
$ codesign --deep --force -vvvv --sign "XXXXXXXXXX" ./myapp
|
||||
```
|
||||
|
||||
We recommend including an `entitlements.plist` file with JIT permissions.
|
||||
|
||||
```xml#entitlements.plist
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.cs.allow-jit</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.disable-executable-page-protection</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.disable-library-validation</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
```
|
||||
|
||||
To codesign with JIT support, pass the `--entitlements` flag to `codesign`.
|
||||
|
||||
```sh
|
||||
$ codesign --deep --force -vvvv --sign "XXXXXXXXXX" --entitlements entitlements.plist ./myapp
|
||||
```
|
||||
|
||||
After codesigning, verify the executable:
|
||||
|
||||
```sh
|
||||
$ codesign -vvv --verify ./myapp
|
||||
./myapp: valid on disk
|
||||
./myapp: satisfies its Designated Requirement
|
||||
```
|
||||
|
||||
{% callout %}
|
||||
|
||||
Codesign support requires Bun v1.2.4 or newer.
|
||||
|
||||
{% /callout %}
|
||||
|
||||
## Unsupported CLI arguments
|
||||
|
||||
Currently, the `--compile` flag can only accept a single entrypoint at a time and does not support the following flags:
|
||||
|
||||
- `--outdir` — use `outfile` instead.
|
||||
- `--splitting`
|
||||
- `--public-path`
|
||||
- `--target=node` or `--target=browser`
|
||||
- `--no-bundle` - we always bundle everything into the executable.
|
||||
@@ -1,99 +0,0 @@
|
||||
# Output styles
|
||||
|
||||
> Adapt Claude Code for uses beyond software engineering
|
||||
|
||||
Output styles allow you to use Claude Code as any type of agent while keeping
|
||||
its core capabilities, such as running local scripts, reading/writing files, and
|
||||
tracking TODOs.
|
||||
|
||||
## Built-in output styles
|
||||
|
||||
Claude Code's **Default** output style is the existing system prompt, designed
|
||||
to help you complete software engineering tasks efficiently.
|
||||
|
||||
There are two additional built-in output styles focused on teaching you the
|
||||
codebase and how Claude operates:
|
||||
|
||||
* **Explanatory**: Provides educational "Insights" in between helping you
|
||||
complete software engineering tasks. Helps you understand implementation
|
||||
choices and codebase patterns.
|
||||
|
||||
* **Learning**: Collaborative, learn-by-doing mode where Claude will not only
|
||||
share "Insights" while coding, but also ask you to contribute small, strategic
|
||||
pieces of code yourself. Claude Code will add `TODO(human)` markers in your
|
||||
code for you to implement.
|
||||
|
||||
## How output styles work
|
||||
|
||||
Output styles directly modify Claude Code's system prompt.
|
||||
|
||||
* Non-default output styles exclude instructions specific to code generation and
|
||||
efficient output normally built into Claude Code (such as responding concisely
|
||||
and verifying code with tests).
|
||||
* Instead, these output styles have their own custom instructions added to the
|
||||
system prompt.
|
||||
|
||||
## Change your output style
|
||||
|
||||
You can either:
|
||||
|
||||
* Run `/output-style` to access the menu and select your output style (this can
|
||||
also be accessed from the `/config` menu)
|
||||
|
||||
* Run `/output-style [style]`, such as `/output-style explanatory`, to directly
|
||||
switch to a style
|
||||
|
||||
These changes apply to the [local project level](/en/docs/claude-code/settings)
|
||||
and are saved in `.claude/settings.local.json`.
|
||||
|
||||
## Create a custom output style
|
||||
|
||||
To set up a new output style with Claude's help, run
|
||||
`/output-style:new I want an output style that ...`
|
||||
|
||||
By default, output styles created through `/output-style:new` are saved as
|
||||
markdown files at the user level in `~/.claude/output-styles` and can be used
|
||||
across projects. They have the following structure:
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: My Custom Style
|
||||
description:
|
||||
A brief description of what this style does, to be displayed to the user
|
||||
---
|
||||
|
||||
# Custom Style Instructions
|
||||
|
||||
You are an interactive CLI tool that helps users with software engineering
|
||||
tasks. [Your custom instructions here...]
|
||||
|
||||
## Specific Behaviors
|
||||
|
||||
[Define how the assistant should behave in this style...]
|
||||
```
|
||||
|
||||
You can also create your own output style Markdown files and save them either at
|
||||
the user level (`~/.claude/output-styles`) or the project level
|
||||
(`.claude/output-styles`).
|
||||
|
||||
## Comparisons to related features
|
||||
|
||||
### Output Styles vs. CLAUDE.md vs. --append-system-prompt
|
||||
|
||||
Output styles completely “turn off” the parts of Claude Code’s default system
|
||||
prompt specific to software engineering. Neither CLAUDE.md nor
|
||||
`--append-system-prompt` edit Claude Code’s default system prompt. CLAUDE.md
|
||||
adds the contents as a user message *following* Claude Code’s default system
|
||||
prompt. `--append-system-prompt` appends the content to the system prompt.
|
||||
|
||||
### Output Styles vs. [Agents](/en/docs/claude-code/sub-agents)
|
||||
|
||||
Output styles directly affect the main agent loop and only affect the system
|
||||
prompt. Agents are invoked to handle specific tasks and can include additional
|
||||
settings like the model to use, the tools they have available, and some context
|
||||
about when to use the agent.
|
||||
|
||||
### Output Styles vs. [Custom Slash Commands](/en/docs/claude-code/slash-commands)
|
||||
|
||||
You can think of output styles as “stored system prompts” and custom slash
|
||||
commands as “stored prompts”.
|
||||
@@ -1,80 +0,0 @@
|
||||
### Project Memory Example
|
||||
|
||||
Claude's context window has limits - long conversations eventually get truncated, and chats don't persist between sessions. Using Chroma as an external memory store solves these limitations, allowing Claude to reference past conversations and maintain context across multiple sessions.
|
||||
|
||||
First, tell Claude to use Chroma for memory as part of the project setup:
|
||||
```
|
||||
Remember, you have access to Chroma tools.
|
||||
At any point if the user references previous chats or memory, check chroma for similar conversations.
|
||||
Try to use retrieved information where possible.
|
||||
```
|
||||
|
||||
This prompt instructs Claude to:
|
||||
- Proactively check Chroma when memory-related topics come up
|
||||
- Search for semantically similar past conversations
|
||||
- Incorporate relevant historical context into responses
|
||||
|
||||
To store the current conversation:
|
||||
|
||||
```
|
||||
Please chunk our conversation into small chunks and store it in Chroma for future reference.
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```
|
||||
can you chunk our entire conversation into small, embeddable text chunks (not the code, but describe it so you can recreate it if necessary). no longer than a couple lines each. then, add it to the chroma collection for this project.
|
||||
```
|
||||
|
||||
Claude will:
|
||||
1. Break the conversation into smaller chunks (typically 512-1024 tokens)
|
||||
- Chunking is necessary because:
|
||||
- Large texts are harder to search semantically
|
||||
- Smaller chunks help retrieve more precise context
|
||||
- It prevents token limits in future retrievals
|
||||
2. Generate embeddings for each chunk
|
||||
3. Add metadata like timestamps and detected topics
|
||||
4. Store everything in your Chroma collection
|
||||
|
||||
Later, you can access past conversations naturally:
|
||||
```
|
||||
What did we discuss previously about the authentication system?
|
||||
```
|
||||
|
||||
Claude will:
|
||||
1. Search Chroma for chunks semantically related to authentication
|
||||
2. Filter by timestamp metadata for last week's discussions
|
||||
3. Incorporate the relevant historical context into its response
|
||||
|
||||
This setup is particularly useful for:
|
||||
- Long-running projects where context gets lost
|
||||
- Teams where multiple people interact with Claude
|
||||
- Complex discussions that reference past decisions
|
||||
- Maintaining consistent context across multiple chat sessions
|
||||
|
||||
### Advanced Features
|
||||
|
||||
The Chroma MCP server supports:
|
||||
|
||||
- **Collection Management**: Create and organize separate collections for different projects
|
||||
- **Document Operations**: Add, update, or delete documents
|
||||
- **Search Capabilities**:
|
||||
- Vector similarity search
|
||||
- Keyword-based search
|
||||
- Metadata filtering
|
||||
- **Batch Processing**: Efficient handling of multiple operations
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If you encounter issues:
|
||||
|
||||
1. Verify your configuration file syntax
|
||||
2. Ensure all paths are absolute and valid
|
||||
3. Try using full paths for `uvx` with `which uvx` and using that path in the config
|
||||
4. Check the Claude logs (paths listed above)
|
||||
|
||||
## Resources
|
||||
|
||||
- [Model Context Protocol Documentation](https://modelcontextprotocol.io/introduction)
|
||||
- [Chroma MCP Server Documentation](https://github.com/chroma-core/chroma-mcp)
|
||||
- [Claude Desktop Guide](https://docs.anthropic.com/claude/docs/claude-desktop)
|
||||
@@ -1,92 +0,0 @@
|
||||
### Team Knowledge Base Example
|
||||
|
||||
Let's say your team maintains a knowledge base of customer support interactions. By storing these in Chroma Cloud, team members can use Claude to quickly access and learn from past support cases.
|
||||
|
||||
First, set up your shared knowledge base:
|
||||
|
||||
```python
|
||||
import chromadb
|
||||
from datetime import datetime
|
||||
|
||||
# Connect to Chroma Cloud
|
||||
client = chromadb.HttpClient(
|
||||
ssl=True,
|
||||
host='api.trychroma.com',
|
||||
tenant='your-tenant-id',
|
||||
database='support-kb',
|
||||
headers={
|
||||
'x-chroma-token': 'YOUR_API_KEY'
|
||||
}
|
||||
)
|
||||
|
||||
# Create a collection for support cases
|
||||
collection = client.create_collection("support_cases")
|
||||
|
||||
# Add some example support cases
|
||||
support_cases = [
|
||||
{
|
||||
"case": "Customer reported issues connecting their IoT devices to the dashboard.",
|
||||
"resolution": "Guided customer through firewall configuration and port forwarding setup.",
|
||||
"category": "connectivity",
|
||||
"date": "2024-03-15"
|
||||
},
|
||||
{
|
||||
"case": "User couldn't access admin features after recent update.",
|
||||
"resolution": "Discovered role permissions weren't migrated correctly. Applied fix and documented process.",
|
||||
"category": "permissions",
|
||||
"date": "2024-03-16"
|
||||
}
|
||||
]
|
||||
|
||||
# Add documents to collection
|
||||
collection.add(
|
||||
documents=[case["case"] + "\n" + case["resolution"] for case in support_cases],
|
||||
metadatas=[{
|
||||
"category": case["category"],
|
||||
"date": case["date"]
|
||||
} for case in support_cases],
|
||||
ids=[f"case_{i}" for i in range(len(support_cases))]
|
||||
)
|
||||
```
|
||||
|
||||
Now team members can use Claude to access this knowledge.
|
||||
|
||||
In your claude config, add the following:
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"chroma": {
|
||||
"command": "uvx",
|
||||
"args": [
|
||||
"chroma-mcp",
|
||||
"--client-type",
|
||||
"cloud",
|
||||
"--tenant",
|
||||
"your-tenant-id",
|
||||
"--database",
|
||||
"support-kb",
|
||||
"--api-key",
|
||||
"YOUR_API_KEY"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Now you can use the knowledge base in your chats:
|
||||
```
|
||||
Claude, I'm having trouble helping a customer with IoT device connectivity.
|
||||
Can you check our support knowledge base for similar cases and suggest a solution?
|
||||
```
|
||||
|
||||
Claude will:
|
||||
1. Search the shared knowledge base for relevant cases
|
||||
2. Consider the context and solutions from similar past issues
|
||||
3. Provide recommendations based on previous successful resolutions
|
||||
|
||||
This setup is particularly powerful because:
|
||||
- All support team members have access to the same knowledge base
|
||||
- Claude can learn from the entire team's experience
|
||||
- Solutions are standardized across the organization
|
||||
- New team members can quickly get up to speed on common issues
|
||||
|
||||
@@ -1,787 +0,0 @@
|
||||
# Hooks reference
|
||||
|
||||
> This page provides reference documentation for implementing hooks in Claude Code.
|
||||
|
||||
<Tip>
|
||||
For a quickstart guide with examples, see [Get started with Claude Code hooks](/en/docs/claude-code/hooks-guide).
|
||||
</Tip>
|
||||
|
||||
## Configuration
|
||||
|
||||
Claude Code hooks are configured in your [settings files](/en/docs/claude-code/settings):
|
||||
|
||||
* `~/.claude/settings.json` - User settings
|
||||
* `.claude/settings.json` - Project settings
|
||||
* `.claude/settings.local.json` - Local project settings (not committed)
|
||||
* Enterprise managed policy settings
|
||||
|
||||
### Structure
|
||||
|
||||
Hooks are organized by matchers, where each matcher can have multiple hooks:
|
||||
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"EventName": [
|
||||
{
|
||||
"matcher": "ToolPattern",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "your-command-here"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
* **matcher**: Pattern to match tool names, case-sensitive (only applicable for
|
||||
`PreToolUse` and `PostToolUse`)
|
||||
* Simple strings match exactly: `Write` matches only the Write tool
|
||||
* Supports regex: `Edit|Write` or `Notebook.*`
|
||||
* Use `*` to match all tools. You can also use empty string (`""`) or leave
|
||||
`matcher` blank.
|
||||
* **hooks**: Array of commands to execute when the pattern matches
|
||||
* `type`: Currently only `"command"` is supported
|
||||
* `command`: The bash command to execute (can use `$CLAUDE_PROJECT_DIR`
|
||||
environment variable)
|
||||
* `timeout`: (Optional) How long a command should run, in seconds, before
|
||||
canceling that specific command.
|
||||
|
||||
For events like `UserPromptSubmit`, `Notification`, `Stop`, and `SubagentStop`
|
||||
that don't use matchers, you can omit the matcher field:
|
||||
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"UserPromptSubmit": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "/path/to/prompt-validator.py"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Project-Specific Hook Scripts
|
||||
|
||||
You can use the environment variable `CLAUDE_PROJECT_DIR` (only available when
|
||||
Claude Code spawns the hook command) to reference scripts stored in your project,
|
||||
ensuring they work regardless of Claude's current directory:
|
||||
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"PostToolUse": [
|
||||
{
|
||||
"matcher": "Write|Edit",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/check-style.sh"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Hook Events
|
||||
|
||||
### PreToolUse
|
||||
|
||||
Runs after Claude creates tool parameters and before processing the tool call.
|
||||
|
||||
**Common matchers:**
|
||||
|
||||
* `Task` - Subagent tasks (see [subagents documentation](/en/docs/claude-code/sub-agents))
|
||||
* `Bash` - Shell commands
|
||||
* `Glob` - File pattern matching
|
||||
* `Grep` - Content search
|
||||
* `Read` - File reading
|
||||
* `Edit`, `MultiEdit` - File editing
|
||||
* `Write` - File writing
|
||||
* `WebFetch`, `WebSearch` - Web operations
|
||||
|
||||
### PostToolUse
|
||||
|
||||
Runs immediately after a tool completes successfully.
|
||||
|
||||
Recognizes the same matcher values as PreToolUse.
|
||||
|
||||
### Notification
|
||||
|
||||
Runs when Claude Code sends notifications. Notifications are sent when:
|
||||
|
||||
1. Claude needs your permission to use a tool. Example: "Claude needs your
|
||||
permission to use Bash"
|
||||
2. The prompt input has been idle for at least 60 seconds. "Claude is waiting
|
||||
for your input"
|
||||
|
||||
### UserPromptSubmit
|
||||
|
||||
Runs when the user submits a prompt, before Claude processes it. This allows you
|
||||
to add additional context based on the prompt/conversation, validate prompts, or
|
||||
block certain types of prompts.
|
||||
|
||||
### Stop
|
||||
|
||||
Runs when the main Claude Code agent has finished responding. Does not run if
|
||||
the stoppage occurred due to a user interrupt.
|
||||
|
||||
### SubagentStop
|
||||
|
||||
Runs when a Claude Code subagent (Task tool call) has finished responding.
|
||||
|
||||
### SessionEnd
|
||||
|
||||
Runs when a Claude Code session ends. Useful for cleanup tasks, logging session
|
||||
statistics, or saving session state.
|
||||
|
||||
The `reason` field in the hook input will be one of:
|
||||
|
||||
* `clear` - Session cleared with /clear command
|
||||
* `logout` - User logged out
|
||||
* `prompt_input_exit` - User exited while prompt input was visible
|
||||
* `other` - Other exit reasons
|
||||
|
||||
### PreCompact
|
||||
|
||||
Runs before Claude Code is about to run a compact operation.
|
||||
|
||||
**Matchers:**
|
||||
|
||||
* `manual` - Invoked from `/compact`
|
||||
* `auto` - Invoked from auto-compact (due to full context window)
|
||||
|
||||
### SessionStart
|
||||
|
||||
Runs when Claude Code starts a new session or resumes an existing session (which
|
||||
currently does start a new session under the hood). Useful for loading in
|
||||
development context like existing issues or recent changes to your codebase.
|
||||
|
||||
**Matchers:**
|
||||
|
||||
* `startup` - Invoked from startup
|
||||
* `resume` - Invoked from `--resume`, `--continue`, or `/resume`
|
||||
* `clear` - Invoked from `/clear`
|
||||
|
||||
## Hook Input
|
||||
|
||||
Hooks receive JSON data via stdin containing session information and
|
||||
event-specific data:
|
||||
|
||||
```typescript
|
||||
{
|
||||
// Common fields
|
||||
session_id: string
|
||||
transcript_path: string // Path to conversation JSON
|
||||
cwd: string // The current working directory when the hook is invoked
|
||||
|
||||
// Event-specific fields
|
||||
hook_event_name: string
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
### PreToolUse Input
|
||||
|
||||
The exact schema for `tool_input` depends on the tool.
|
||||
|
||||
```json
|
||||
{
|
||||
"session_id": "abc123",
|
||||
"transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
|
||||
"cwd": "/Users/...",
|
||||
"hook_event_name": "PreToolUse",
|
||||
"tool_name": "Write",
|
||||
"tool_input": {
|
||||
"file_path": "/path/to/file.txt",
|
||||
"content": "file content"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### PostToolUse Input
|
||||
|
||||
The exact schema for `tool_input` and `tool_response` depends on the tool.
|
||||
|
||||
```json
|
||||
{
|
||||
"session_id": "abc123",
|
||||
"transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
|
||||
"cwd": "/Users/...",
|
||||
"hook_event_name": "PostToolUse",
|
||||
"tool_name": "Write",
|
||||
"tool_input": {
|
||||
"file_path": "/path/to/file.txt",
|
||||
"content": "file content"
|
||||
},
|
||||
"tool_response": {
|
||||
"filePath": "/path/to/file.txt",
|
||||
"success": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Notification Input
|
||||
|
||||
```json
|
||||
{
|
||||
"session_id": "abc123",
|
||||
"transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
|
||||
"cwd": "/Users/...",
|
||||
"hook_event_name": "Notification",
|
||||
"message": "Task completed successfully"
|
||||
}
|
||||
```
|
||||
|
||||
### UserPromptSubmit Input
|
||||
|
||||
```json
|
||||
{
|
||||
"session_id": "abc123",
|
||||
"transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
|
||||
"cwd": "/Users/...",
|
||||
"hook_event_name": "UserPromptSubmit",
|
||||
"prompt": "Write a function to calculate the factorial of a number"
|
||||
}
|
||||
```
|
||||
|
||||
### Stop and SubagentStop Input
|
||||
|
||||
`stop_hook_active` is true when Claude Code is already continuing as a result of
|
||||
a stop hook. Check this value or process the transcript to prevent Claude Code
|
||||
from running indefinitely.
|
||||
|
||||
```json
|
||||
{
|
||||
"session_id": "abc123",
|
||||
"transcript_path": "~/.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
|
||||
"hook_event_name": "Stop",
|
||||
"stop_hook_active": true
|
||||
}
|
||||
```
|
||||
|
||||
### PreCompact Input
|
||||
|
||||
For `manual`, `custom_instructions` comes from what the user passes into
|
||||
`/compact`. For `auto`, `custom_instructions` is empty.
|
||||
|
||||
```json
|
||||
{
|
||||
"session_id": "abc123",
|
||||
"transcript_path": "~/.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
|
||||
"hook_event_name": "PreCompact",
|
||||
"trigger": "manual",
|
||||
"custom_instructions": ""
|
||||
}
|
||||
```
|
||||
|
||||
### SessionStart Input
|
||||
|
||||
```json
|
||||
{
|
||||
"session_id": "abc123",
|
||||
"transcript_path": "~/.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
|
||||
"hook_event_name": "SessionStart",
|
||||
"source": "startup"
|
||||
}
|
||||
```
|
||||
|
||||
### SessionEnd Input
|
||||
|
||||
```json
|
||||
{
|
||||
"session_id": "abc123",
|
||||
"transcript_path": "~/.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
|
||||
"cwd": "/Users/...",
|
||||
"hook_event_name": "SessionEnd",
|
||||
"reason": "exit"
|
||||
}
|
||||
```
|
||||
|
||||
## Hook Output
|
||||
|
||||
There are two ways for hooks to return output back to Claude Code. The output
|
||||
communicates whether to block and any feedback that should be shown to Claude
|
||||
and the user.
|
||||
|
||||
### Simple: Exit Code
|
||||
|
||||
Hooks communicate status through exit codes, stdout, and stderr:
|
||||
|
||||
* **Exit code 0**: Success. `stdout` is shown to the user in transcript mode
|
||||
(CTRL-R), except for `UserPromptSubmit` and `SessionStart`, where stdout is
|
||||
added to the context.
|
||||
* **Exit code 2**: Blocking error. `stderr` is fed back to Claude to process
|
||||
automatically. See per-hook-event behavior below.
|
||||
* **Other exit codes**: Non-blocking error. `stderr` is shown to the user and
|
||||
execution continues.
|
||||
|
||||
<Warning>
|
||||
Reminder: Claude Code does not see stdout if the exit code is 0, except for
|
||||
the `UserPromptSubmit` hook where stdout is injected as context.
|
||||
</Warning>
|
||||
|
||||
#### Exit Code 2 Behavior
|
||||
|
||||
| Hook Event | Behavior |
|
||||
| ------------------ | ------------------------------------------------------------------ |
|
||||
| `PreToolUse` | Blocks the tool call, shows stderr to Claude |
|
||||
| `PostToolUse` | Shows stderr to Claude (tool already ran) |
|
||||
| `Notification` | N/A, shows stderr to user only |
|
||||
| `UserPromptSubmit` | Blocks prompt processing, erases prompt, shows stderr to user only |
|
||||
| `Stop` | Blocks stoppage, shows stderr to Claude |
|
||||
| `SubagentStop` | Blocks stoppage, shows stderr to Claude subagent |
|
||||
| `PreCompact` | N/A, shows stderr to user only |
|
||||
| `SessionStart` | N/A, shows stderr to user only |
|
||||
| `SessionEnd` | N/A, shows stderr to user only |
|
||||
|
||||
### Advanced: JSON Output
|
||||
|
||||
Hooks can return structured JSON in `stdout` for more sophisticated control:
|
||||
|
||||
#### Common JSON Fields
|
||||
|
||||
All hook types can include these optional fields:
|
||||
|
||||
```json
|
||||
{
|
||||
"continue": true, // Whether Claude should continue after hook execution (default: true)
|
||||
"stopReason": "string", // Message shown when continue is false
|
||||
|
||||
"suppressOutput": true, // Hide stdout from transcript mode (default: false)
|
||||
"systemMessage": "string" // Optional warning message shown to the user
|
||||
}
|
||||
```
|
||||
|
||||
If `continue` is false, Claude stops processing after the hooks run.
|
||||
|
||||
* For `PreToolUse`, this is different from `"permissionDecision": "deny"`, which
|
||||
only blocks a specific tool call and provides automatic feedback to Claude.
|
||||
* For `PostToolUse`, this is different from `"decision": "block"`, which
|
||||
provides automated feedback to Claude.
|
||||
* For `UserPromptSubmit`, this prevents the prompt from being processed.
|
||||
* For `Stop` and `SubagentStop`, this takes precedence over any
|
||||
`"decision": "block"` output.
|
||||
* In all cases, `"continue" = false` takes precedence over any
|
||||
`"decision": "block"` output.
|
||||
|
||||
`stopReason` accompanies `continue` with a reason shown to the user, not shown
|
||||
to Claude.
|
||||
|
||||
#### `PreToolUse` Decision Control
|
||||
|
||||
`PreToolUse` hooks can control whether a tool call proceeds.
|
||||
|
||||
* `"allow"` bypasses the permission system. `permissionDecisionReason` is shown
|
||||
to the user but not to Claude.
|
||||
* `"deny"` prevents the tool call from executing. `permissionDecisionReason` is
|
||||
shown to Claude.
|
||||
* `"ask"` asks the user to confirm the tool call in the UI.
|
||||
`permissionDecisionReason` is shown to the user but not to Claude.
|
||||
|
||||
```json
|
||||
{
|
||||
"hookSpecificOutput": {
|
||||
"hookEventName": "PreToolUse",
|
||||
"permissionDecision": "allow" | "deny" | "ask",
|
||||
"permissionDecisionReason": "My reason here"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<Note>
|
||||
The `decision` and `reason` fields are deprecated for PreToolUse hooks.
|
||||
Use `hookSpecificOutput.permissionDecision` and
|
||||
`hookSpecificOutput.permissionDecisionReason` instead. The deprecated fields
|
||||
`"approve"` and `"block"` map to `"allow"` and `"deny"` respectively.
|
||||
</Note>
|
||||
|
||||
#### `PostToolUse` Decision Control
|
||||
|
||||
`PostToolUse` hooks can provide feedback to Claude after tool execution.
|
||||
|
||||
* `"block"` automatically prompts Claude with `reason`.
|
||||
* `undefined` does nothing. `reason` is ignored.
|
||||
* `"hookSpecificOutput.additionalContext"` adds context for Claude to consider.
|
||||
|
||||
```json
|
||||
{
|
||||
"decision": "block" | undefined,
|
||||
"reason": "Explanation for decision",
|
||||
"hookSpecificOutput": {
|
||||
"hookEventName": "PostToolUse",
|
||||
"additionalContext": "Additional information for Claude"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### `UserPromptSubmit` Decision Control
|
||||
|
||||
`UserPromptSubmit` hooks can control whether a user prompt is processed.
|
||||
|
||||
* `"block"` prevents the prompt from being processed. The submitted prompt is
|
||||
erased from context. `"reason"` is shown to the user but not added to context.
|
||||
* `undefined` allows the prompt to proceed normally. `"reason"` is ignored.
|
||||
* `"hookSpecificOutput.additionalContext"` adds the string to the context if not
|
||||
blocked.
|
||||
|
||||
```json
|
||||
{
|
||||
"decision": "block" | undefined,
|
||||
"reason": "Explanation for decision",
|
||||
"hookSpecificOutput": {
|
||||
"hookEventName": "UserPromptSubmit",
|
||||
"additionalContext": "My additional context here"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### `Stop`/`SubagentStop` Decision Control
|
||||
|
||||
`Stop` and `SubagentStop` hooks can control whether Claude must continue.
|
||||
|
||||
* `"block"` prevents Claude from stopping. You must populate `reason` for Claude
|
||||
to know how to proceed.
|
||||
* `undefined` allows Claude to stop. `reason` is ignored.
|
||||
|
||||
```json
|
||||
{
|
||||
"decision": "block" | undefined,
|
||||
"reason": "Must be provided when Claude is blocked from stopping"
|
||||
}
|
||||
```
|
||||
|
||||
#### `SessionStart` Decision Control
|
||||
|
||||
`SessionStart` hooks allow you to load in context at the start of a session.
|
||||
|
||||
* `"hookSpecificOutput.additionalContext"` adds the string to the context.
|
||||
* Multiple hooks' `additionalContext` values are concatenated.
|
||||
|
||||
```json
|
||||
{
|
||||
"hookSpecificOutput": {
|
||||
"hookEventName": "SessionStart",
|
||||
"additionalContext": "My additional context here"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### `SessionEnd` Decision Control
|
||||
|
||||
`SessionEnd` hooks run when a session ends. They cannot block session termination
|
||||
but can perform cleanup tasks.
|
||||
|
||||
#### Exit Code Example: Bash Command Validation
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
|
||||
# Define validation rules as a list of (regex pattern, message) tuples
|
||||
VALIDATION_RULES = [
|
||||
(
|
||||
r"\bgrep\b(?!.*\|)",
|
||||
"Use 'rg' (ripgrep) instead of 'grep' for better performance and features",
|
||||
),
|
||||
(
|
||||
r"\bfind\s+\S+\s+-name\b",
|
||||
"Use 'rg --files | rg pattern' or 'rg --files -g pattern' instead of 'find -name' for better performance",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def validate_command(command: str) -> list[str]:
|
||||
issues = []
|
||||
for pattern, message in VALIDATION_RULES:
|
||||
if re.search(pattern, command):
|
||||
issues.append(message)
|
||||
return issues
|
||||
|
||||
|
||||
try:
|
||||
input_data = json.load(sys.stdin)
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"Error: Invalid JSON input: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
tool_name = input_data.get("tool_name", "")
|
||||
tool_input = input_data.get("tool_input", {})
|
||||
command = tool_input.get("command", "")
|
||||
|
||||
if tool_name != "Bash" or not command:
|
||||
sys.exit(1)
|
||||
|
||||
# Validate the command
|
||||
issues = validate_command(command)
|
||||
|
||||
if issues:
|
||||
for message in issues:
|
||||
print(f"• {message}", file=sys.stderr)
|
||||
# Exit code 2 blocks tool call and shows stderr to Claude
|
||||
sys.exit(2)
|
||||
```
|
||||
|
||||
#### JSON Output Example: UserPromptSubmit to Add Context and Validation
|
||||
|
||||
<Note>
|
||||
For `UserPromptSubmit` hooks, you can inject context using either method:
|
||||
|
||||
* Exit code 0 with stdout: Claude sees the context (special case for `UserPromptSubmit`)
|
||||
* JSON output: Provides more control over the behavior
|
||||
</Note>
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
import sys
|
||||
import re
|
||||
import datetime
|
||||
|
||||
# Load input from stdin
|
||||
try:
|
||||
input_data = json.load(sys.stdin)
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"Error: Invalid JSON input: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
prompt = input_data.get("prompt", "")
|
||||
|
||||
# Check for sensitive patterns
|
||||
sensitive_patterns = [
|
||||
(r"(?i)\b(password|secret|key|token)\s*[:=]", "Prompt contains potential secrets"),
|
||||
]
|
||||
|
||||
for pattern, message in sensitive_patterns:
|
||||
if re.search(pattern, prompt):
|
||||
# Use JSON output to block with a specific reason
|
||||
output = {
|
||||
"decision": "block",
|
||||
"reason": f"Security policy violation: {message}. Please rephrase your request without sensitive information."
|
||||
}
|
||||
print(json.dumps(output))
|
||||
sys.exit(0)
|
||||
|
||||
# Add current time to context
|
||||
context = f"Current time: {datetime.datetime.now()}"
|
||||
print(context)
|
||||
|
||||
"""
|
||||
The following is also equivalent:
|
||||
print(json.dumps({
|
||||
"hookSpecificOutput": {
|
||||
"hookEventName": "UserPromptSubmit",
|
||||
"additionalContext": context,
|
||||
},
|
||||
}))
|
||||
"""
|
||||
|
||||
# Allow the prompt to proceed with the additional context
|
||||
sys.exit(0)
|
||||
```
|
||||
|
||||
#### JSON Output Example: PreToolUse with Approval
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
import sys
|
||||
|
||||
# Load input from stdin
|
||||
try:
|
||||
input_data = json.load(sys.stdin)
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"Error: Invalid JSON input: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
tool_name = input_data.get("tool_name", "")
|
||||
tool_input = input_data.get("tool_input", {})
|
||||
|
||||
# Example: Auto-approve file reads for documentation files
|
||||
if tool_name == "Read":
|
||||
file_path = tool_input.get("file_path", "")
|
||||
if file_path.endswith((".md", ".mdx", ".txt", ".json")):
|
||||
# Use JSON output to auto-approve the tool call
|
||||
output = {
|
||||
"decision": "approve",
|
||||
"reason": "Documentation file auto-approved",
|
||||
"suppressOutput": True # Don't show in transcript mode
|
||||
}
|
||||
print(json.dumps(output))
|
||||
sys.exit(0)
|
||||
|
||||
# For other cases, let the normal permission flow proceed
|
||||
sys.exit(0)
|
||||
```
|
||||
|
||||
## Working with MCP Tools
|
||||
|
||||
Claude Code hooks work seamlessly with
|
||||
[Model Context Protocol (MCP) tools](/en/docs/claude-code/mcp). When MCP servers
|
||||
provide tools, they appear with a special naming pattern that you can match in
|
||||
your hooks.
|
||||
|
||||
### MCP Tool Naming
|
||||
|
||||
MCP tools follow the pattern `mcp__<server>__<tool>`, for example:
|
||||
|
||||
* `mcp__memory__create_entities` - Memory server's create entities tool
|
||||
* `mcp__filesystem__read_file` - Filesystem server's read file tool
|
||||
* `mcp__github__search_repositories` - GitHub server's search tool
|
||||
|
||||
### Configuring Hooks for MCP Tools
|
||||
|
||||
You can target specific MCP tools or entire MCP servers:
|
||||
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"PreToolUse": [
|
||||
{
|
||||
"matcher": "mcp__memory__.*",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "echo 'Memory operation initiated' >> ~/mcp-operations.log"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": "mcp__.*__write.*",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "/home/user/scripts/validate-mcp-write.py"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
<Tip>
|
||||
For practical examples including code formatting, notifications, and file protection, see [More Examples](/en/docs/claude-code/hooks-guide#more-examples) in the get started guide.
|
||||
</Tip>
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### Disclaimer
|
||||
|
||||
**USE AT YOUR OWN RISK**: Claude Code hooks execute arbitrary shell commands on
|
||||
your system automatically. By using hooks, you acknowledge that:
|
||||
|
||||
* You are solely responsible for the commands you configure
|
||||
* Hooks can modify, delete, or access any files your user account can access
|
||||
* Malicious or poorly written hooks can cause data loss or system damage
|
||||
* Anthropic provides no warranty and assumes no liability for any damages
|
||||
resulting from hook usage
|
||||
* You should thoroughly test hooks in a safe environment before production use
|
||||
|
||||
Always review and understand any hook commands before adding them to your
|
||||
configuration.
|
||||
|
||||
### Security Best Practices
|
||||
|
||||
Here are some key practices for writing more secure hooks:
|
||||
|
||||
1. **Validate and sanitize inputs** - Never trust input data blindly
|
||||
2. **Always quote shell variables** - Use `"$VAR"` not `$VAR`
|
||||
3. **Block path traversal** - Check for `..` in file paths
|
||||
4. **Use absolute paths** - Specify full paths for scripts (use
|
||||
`$CLAUDE_PROJECT_DIR` for the project path)
|
||||
5. **Skip sensitive files** - Avoid `.env`, `.git/`, keys, etc.
|
||||
|
||||
### Configuration Safety
|
||||
|
||||
Direct edits to hooks in settings files don't take effect immediately. Claude
|
||||
Code:
|
||||
|
||||
1. Captures a snapshot of hooks at startup
|
||||
2. Uses this snapshot throughout the session
|
||||
3. Warns if hooks are modified externally
|
||||
4. Requires review in `/hooks` menu for changes to apply
|
||||
|
||||
This prevents malicious hook modifications from affecting your current session.
|
||||
|
||||
## Hook Execution Details
|
||||
|
||||
* **Timeout**: 60-second execution limit by default, configurable per command.
|
||||
* A timeout for an individual command does not affect the other commands.
|
||||
* **Parallelization**: All matching hooks run in parallel
|
||||
* **Deduplication**: Multiple identical hook commands are deduplicated automatically
|
||||
* **Environment**: Runs in current directory with Claude Code's environment
|
||||
* The `CLAUDE_PROJECT_DIR` environment variable is available and contains the
|
||||
absolute path to the project root directory (where Claude Code was started)
|
||||
* **Input**: JSON via stdin
|
||||
* **Output**:
|
||||
* PreToolUse/PostToolUse/Stop/SubagentStop: Progress shown in transcript (Ctrl-R)
|
||||
* Notification/SessionEnd: Logged to debug only (`--debug`)
|
||||
* UserPromptSubmit/SessionStart: stdout added as context for Claude
|
||||
|
||||
## Debugging
|
||||
|
||||
### Basic Troubleshooting
|
||||
|
||||
If your hooks aren't working:
|
||||
|
||||
1. **Check configuration** - Run `/hooks` to see if your hook is registered
|
||||
2. **Verify syntax** - Ensure your JSON settings are valid
|
||||
3. **Test commands** - Run hook commands manually first
|
||||
4. **Check permissions** - Make sure scripts are executable
|
||||
5. **Review logs** - Use `claude --debug` to see hook execution details
|
||||
|
||||
Common issues:
|
||||
|
||||
* **Quotes not escaped** - Use `\"` inside JSON strings
|
||||
* **Wrong matcher** - Check tool names match exactly (case-sensitive)
|
||||
* **Command not found** - Use full paths for scripts
|
||||
|
||||
### Advanced Debugging
|
||||
|
||||
For complex hook issues:
|
||||
|
||||
1. **Inspect hook execution** - Use `claude --debug` to see detailed hook
|
||||
execution
|
||||
2. **Validate JSON schemas** - Test hook input/output with external tools
|
||||
3. **Check environment variables** - Verify Claude Code's environment is correct
|
||||
4. **Test edge cases** - Try hooks with unusual file paths or inputs
|
||||
5. **Monitor system resources** - Check for resource exhaustion during hook
|
||||
execution
|
||||
6. **Use structured logging** - Implement logging in your hook scripts
|
||||
|
||||
### Debug Output Example
|
||||
|
||||
Use `claude --debug` to see hook execution details:
|
||||
|
||||
```
|
||||
[DEBUG] Executing hooks for PostToolUse:Write
|
||||
[DEBUG] Getting matching hook commands for PostToolUse with query: Write
|
||||
[DEBUG] Found 1 hook matchers in settings
|
||||
[DEBUG] Matched 1 hooks for query "Write"
|
||||
[DEBUG] Found 1 hook commands to execute
|
||||
[DEBUG] Executing hook command: <Your command> with timeout 60000ms
|
||||
[DEBUG] Hook command completed with status 0: <Your stdout>
|
||||
```
|
||||
|
||||
Progress messages appear in transcript mode (Ctrl-R) showing:
|
||||
|
||||
* Which hook is running
|
||||
* Command being executed
|
||||
* Success/failure status
|
||||
* Output or error messages
|
||||
@@ -1,202 +0,0 @@
|
||||
# Status line configuration
|
||||
|
||||
> Create a custom status line for Claude Code to display contextual information
|
||||
|
||||
Make Claude Code your own with a custom status line that displays at the bottom of the Claude Code interface, similar to how terminal prompts (PS1) work in shells like Oh-my-zsh.
|
||||
|
||||
## Create a custom status line
|
||||
|
||||
You can either:
|
||||
|
||||
* Run `/statusline` to ask Claude Code to help you set up a custom status line. By default, it will try to reproduce your terminal's prompt, but you can provide additional instructions about the behavior you want to Claude Code, such as `/statusline show the model name in orange`
|
||||
|
||||
* Directly add a `statusLine` command to your `.claude/settings.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"statusLine": {
|
||||
"type": "command",
|
||||
"command": "~/.claude/statusline.sh",
|
||||
"padding": 0 // Optional: set to 0 to let status line go to edge
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## How it Works
|
||||
|
||||
* The status line is updated when the conversation messages update
|
||||
* Updates run at most every 300ms
|
||||
* The first line of stdout from your command becomes the status line text
|
||||
* ANSI color codes are supported for styling your status line
|
||||
* Claude Code passes contextual information about the current session (model, directories, etc.) as JSON to your script via stdin
|
||||
|
||||
## JSON Input Structure
|
||||
|
||||
Your status line command receives structured data via stdin in JSON format:
|
||||
|
||||
```json
|
||||
{
|
||||
"hook_event_name": "Status",
|
||||
"session_id": "abc123...",
|
||||
"transcript_path": "/path/to/transcript.json",
|
||||
"cwd": "/current/working/directory",
|
||||
"model": {
|
||||
"id": "claude-opus-4-1",
|
||||
"display_name": "Opus"
|
||||
},
|
||||
"workspace": {
|
||||
"current_dir": "/current/working/directory",
|
||||
"project_dir": "/original/project/directory"
|
||||
},
|
||||
"version": "1.0.80",
|
||||
"output_style": {
|
||||
"name": "default"
|
||||
},
|
||||
"cost": {
|
||||
"total_cost_usd": 0.01234,
|
||||
"total_duration_ms": 45000,
|
||||
"total_api_duration_ms": 2300,
|
||||
"total_lines_added": 156,
|
||||
"total_lines_removed": 23
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Example Scripts
|
||||
|
||||
### Simple Status Line
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# Read JSON input from stdin
|
||||
input=$(cat)
|
||||
|
||||
# Extract values using jq
|
||||
MODEL_DISPLAY=$(echo "$input" | jq -r '.model.display_name')
|
||||
CURRENT_DIR=$(echo "$input" | jq -r '.workspace.current_dir')
|
||||
|
||||
echo "[$MODEL_DISPLAY] 📁 ${CURRENT_DIR##*/}"
|
||||
```
|
||||
|
||||
### Git-Aware Status Line
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# Read JSON input from stdin
|
||||
input=$(cat)
|
||||
|
||||
# Extract values using jq
|
||||
MODEL_DISPLAY=$(echo "$input" | jq -r '.model.display_name')
|
||||
CURRENT_DIR=$(echo "$input" | jq -r '.workspace.current_dir')
|
||||
|
||||
# Show git branch if in a git repo
|
||||
GIT_BRANCH=""
|
||||
if git rev-parse --git-dir > /dev/null 2>&1; then
|
||||
BRANCH=$(git branch --show-current 2>/dev/null)
|
||||
if [ -n "$BRANCH" ]; then
|
||||
GIT_BRANCH=" | 🌿 $BRANCH"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "[$MODEL_DISPLAY] 📁 ${CURRENT_DIR##*/}$GIT_BRANCH"
|
||||
```
|
||||
|
||||
### Python Example
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Read JSON from stdin
|
||||
data = json.load(sys.stdin)
|
||||
|
||||
# Extract values
|
||||
model = data['model']['display_name']
|
||||
current_dir = os.path.basename(data['workspace']['current_dir'])
|
||||
|
||||
# Check for git branch
|
||||
git_branch = ""
|
||||
if os.path.exists('.git'):
|
||||
try:
|
||||
with open('.git/HEAD', 'r') as f:
|
||||
ref = f.read().strip()
|
||||
if ref.startswith('ref: refs/heads/'):
|
||||
git_branch = f" | 🌿 {ref.replace('ref: refs/heads/', '')}"
|
||||
except:
|
||||
pass
|
||||
|
||||
print(f"[{model}] 📁 {current_dir}{git_branch}")
|
||||
```
|
||||
|
||||
### Node.js Example
|
||||
|
||||
```javascript
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// Read JSON from stdin
|
||||
let input = '';
|
||||
process.stdin.on('data', chunk => input += chunk);
|
||||
process.stdin.on('end', () => {
|
||||
const data = JSON.parse(input);
|
||||
|
||||
// Extract values
|
||||
const model = data.model.display_name;
|
||||
const currentDir = path.basename(data.workspace.current_dir);
|
||||
|
||||
// Check for git branch
|
||||
let gitBranch = '';
|
||||
try {
|
||||
const headContent = fs.readFileSync('.git/HEAD', 'utf8').trim();
|
||||
if (headContent.startsWith('ref: refs/heads/')) {
|
||||
gitBranch = ` | 🌿 ${headContent.replace('ref: refs/heads/', '')}`;
|
||||
}
|
||||
} catch (e) {
|
||||
// Not a git repo or can't read HEAD
|
||||
}
|
||||
|
||||
console.log(`[${model}] 📁 ${currentDir}${gitBranch}`);
|
||||
});
|
||||
```
|
||||
|
||||
### Helper Function Approach
|
||||
|
||||
For more complex bash scripts, you can create helper functions:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# Read JSON input once
|
||||
input=$(cat)
|
||||
|
||||
# Helper functions for common extractions
|
||||
get_model_name() { echo "$input" | jq -r '.model.display_name'; }
|
||||
get_current_dir() { echo "$input" | jq -r '.workspace.current_dir'; }
|
||||
get_project_dir() { echo "$input" | jq -r '.workspace.project_dir'; }
|
||||
get_version() { echo "$input" | jq -r '.version'; }
|
||||
get_cost() { echo "$input" | jq -r '.cost.total_cost_usd'; }
|
||||
get_duration() { echo "$input" | jq -r '.cost.total_duration_ms'; }
|
||||
get_lines_added() { echo "$input" | jq -r '.cost.total_lines_added'; }
|
||||
get_lines_removed() { echo "$input" | jq -r '.cost.total_lines_removed'; }
|
||||
|
||||
# Use the helpers
|
||||
MODEL=$(get_model_name)
|
||||
DIR=$(get_current_dir)
|
||||
echo "[$MODEL] 📁 ${DIR##*/}"
|
||||
```
|
||||
|
||||
## Tips
|
||||
|
||||
* Keep your status line concise - it should fit on one line
|
||||
* Use emojis (if your terminal supports them) and colors to make information scannable
|
||||
* Use `jq` for JSON parsing in Bash (see examples above)
|
||||
* Test your script by running it manually with mock JSON input: `echo '{"model":{"display_name":"Test"},"workspace":{"current_dir":"/test"}}' | ./statusline.sh`
|
||||
* Consider caching expensive operations (like git status) if needed
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
* If your status line doesn't appear, check that your script is executable (`chmod +x`)
|
||||
* Ensure your script outputs to stdout (not stderr)
|
||||
@@ -1,173 +0,0 @@
|
||||
# Claude Code Hook Configuration Documentation
|
||||
|
||||
**LOCKED by @docs-agent | Change to 🔑 to allow @docs-agent edits**
|
||||
|
||||
## Official Documentation Reference
|
||||
|
||||
- **Source**: Claude Code Hooks API Documentation
|
||||
- **Version**: v2025
|
||||
- **Last Verified**: 2025-08-31
|
||||
- **Official URL**: https://docs.anthropic.com/en/docs/claude-code/hooks
|
||||
|
||||
## Hook Configuration Structure
|
||||
|
||||
### Two Categories of Hooks
|
||||
|
||||
Claude Code hooks are divided into two distinct categories with different configuration structures:
|
||||
|
||||
#### 1. Tool-Related Hooks
|
||||
These hooks are triggered in relation to tool usage and require a `matcher` field:
|
||||
- `PreToolUse`: Executed before a tool is invoked
|
||||
- `PostToolUse`: Executed after a tool completes
|
||||
|
||||
**Configuration Structure:**
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"PreToolUse": [
|
||||
{
|
||||
"matcher": "Edit|MultiEdit|Write",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "/path/to/script.js",
|
||||
"timeout": 60000
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. Non-Tool Hooks
|
||||
These hooks are triggered by system events and **MUST NOT** have a `matcher` or `pattern` field:
|
||||
- `PreCompact`: Before conversation compaction
|
||||
- `SessionStart`: When a new session begins
|
||||
- `SessionEnd`: When a session ends
|
||||
- `UserPromptSubmit`: When user submits a prompt
|
||||
- `Notification`: For system notifications
|
||||
- `Stop`: When Claude is stopping
|
||||
- `SubagentStop`: When a subagent is stopping
|
||||
|
||||
**Configuration Structure:**
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"SessionStart": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "/path/to/script.js",
|
||||
"timeout": 30000
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Common Configuration Mistakes
|
||||
|
||||
### ❌ INCORRECT: Adding `pattern` field to non-tool hooks
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"PreCompact": [
|
||||
{
|
||||
"pattern": "*", // WRONG: Non-tool hooks don't use patterns
|
||||
"hooks": [...]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### ✅ CORRECT: Non-tool hooks without matcher
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"PreCompact": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "/path/to/pre-compact.js",
|
||||
"timeout": 180000
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Hook Field Reference
|
||||
|
||||
### Common Fields (All Hooks)
|
||||
- `type`: Always `"command"` for external scripts
|
||||
- `command`: Absolute path to the executable script
|
||||
- `timeout`: Optional timeout in milliseconds (default: 60000)
|
||||
|
||||
### Tool Hook Specific
|
||||
- `matcher`: Regex pattern to match tool names
|
||||
- Example: `"Edit|MultiEdit|Write"`
|
||||
- Example: `"mcp__.*__write.*"`
|
||||
- Example: `"Bash"`
|
||||
|
||||
### Environment Variables Available to Hooks
|
||||
- `$CLAUDE_PROJECT_DIR`: Project root directory
|
||||
- Standard environment variables from the shell
|
||||
|
||||
## Hook Input/Output
|
||||
|
||||
### Input (via stdin)
|
||||
All hooks receive JSON input with common fields:
|
||||
```json
|
||||
{
|
||||
"session_id": "string",
|
||||
"transcript_path": "string",
|
||||
"cwd": "string",
|
||||
"hook_event_name": "string",
|
||||
// Additional event-specific fields
|
||||
}
|
||||
```
|
||||
|
||||
### Output Options
|
||||
Hooks can output:
|
||||
1. **Plain text** (stdout): Added as context
|
||||
2. **JSON** (stdout): Structured response for decisions
|
||||
3. **Exit codes**:
|
||||
- `0`: Success, continue normally
|
||||
- `1`: General error
|
||||
- `2`: Block operation (for PreToolUse)
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
### File Locations
|
||||
- User settings: `~/.claude/settings.json`
|
||||
- Project settings: `./.claude/settings.json`
|
||||
- Local settings: `./.claude/settings.local.json`
|
||||
|
||||
### Settings Precedence (Highest to Lowest)
|
||||
1. Enterprise managed policies
|
||||
2. Command line arguments
|
||||
3. Local project settings
|
||||
4. Shared project settings
|
||||
5. User settings
|
||||
|
||||
## Cross-References
|
||||
|
||||
- Code Implementation: `/Users/alexnewman/Scripts/claude-mem/src/commands/install.ts:263-320`
|
||||
- Hook Files: `/Users/alexnewman/Scripts/claude-mem/hooks/`
|
||||
- User Guide: `/Users/alexnewman/Scripts/claude-mem/README-npm.md`
|
||||
|
||||
## Version History
|
||||
|
||||
- **2025-08-31**: Fixed hook configuration to remove incorrect `pattern` field from non-tool hooks
|
||||
- **2025-08-31**: Documented official hook structure requirements per Claude Code API
|
||||
|
||||
---
|
||||
*This documentation is maintained by @docs-agent and verified against official Anthropic documentation.*
|
||||
@@ -1,127 +0,0 @@
|
||||
# Claude Code Hook Response Format Documentation
|
||||
## Source: Official Claude Code Docs v2025
|
||||
## Last Verified: 2025-08-31
|
||||
|
||||
## Common Hook Response Fields
|
||||
|
||||
All hooks can return these common fields:
|
||||
|
||||
```json
|
||||
{
|
||||
"continue": true, // Whether Claude should continue (default: true)
|
||||
"stopReason": "string", // Message shown when continue is false
|
||||
"suppressOutput": true, // Hide stdout from transcript (default: false)
|
||||
"systemMessage": "string" // Optional warning message shown to user
|
||||
}
|
||||
```
|
||||
|
||||
## Hook-Specific Response Formats
|
||||
|
||||
### PreCompact Hook
|
||||
**IMPORTANT**: PreCompact does NOT support `hookSpecificOutput`
|
||||
|
||||
```json
|
||||
{
|
||||
"continue": true,
|
||||
"suppressOutput": true
|
||||
}
|
||||
```
|
||||
|
||||
### SessionStart Hook
|
||||
SessionStart DOES support `hookSpecificOutput`:
|
||||
|
||||
```json
|
||||
{
|
||||
"continue": true,
|
||||
"hookSpecificOutput": {
|
||||
"hookEventName": "SessionStart",
|
||||
"additionalContext": "Context string to add to session"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### PreToolUse Hook
|
||||
```json
|
||||
{
|
||||
"continue": true,
|
||||
"hookSpecificOutput": {
|
||||
"hookEventName": "PreToolUse",
|
||||
"permissionDecision": "allow" | "deny" | "ask",
|
||||
"permissionDecisionReason": "Reason for decision"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### PostToolUse Hook
|
||||
```json
|
||||
{
|
||||
"decision": "block", // Optional - blocks further processing
|
||||
"reason": "Explanation",
|
||||
"hookSpecificOutput": {
|
||||
"hookEventName": "PostToolUse",
|
||||
"additionalContext": "Additional information for Claude"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### UserPromptSubmit Hook
|
||||
```json
|
||||
{
|
||||
"decision": "block", // Optional - blocks the prompt
|
||||
"reason": "Security policy violation",
|
||||
"hookSpecificOutput": {
|
||||
"hookEventName": "UserPromptSubmit",
|
||||
"additionalContext": "Additional context for the prompt"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Exit Codes
|
||||
|
||||
- `0`: Success - hook executed successfully
|
||||
- `1`: Error - shown to user with stdout
|
||||
- `2`: Error - shown to Claude with stderr
|
||||
|
||||
## Common Mistakes to Avoid
|
||||
|
||||
### \u274c INCORRECT: Using wrong field names
|
||||
```javascript
|
||||
// WRONG
|
||||
{
|
||||
"decision": "block", // \u274c Wrong field
|
||||
"reason": "Error message" // \u274c Wrong field
|
||||
}
|
||||
```
|
||||
|
||||
### \u2705 CORRECT: Using official field names
|
||||
```javascript
|
||||
// RIGHT
|
||||
{
|
||||
"continue": false,
|
||||
"stopReason": "Error message"
|
||||
}
|
||||
```
|
||||
|
||||
### \u274c INCORRECT: Adding hookSpecificOutput to PreCompact
|
||||
```javascript
|
||||
// WRONG - PreCompact doesn't support this
|
||||
{
|
||||
"hookSpecificOutput": {
|
||||
"hookEventName": "PreCompact",
|
||||
"status": "success"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### \u2705 CORRECT: Simple response for PreCompact
|
||||
```javascript
|
||||
// RIGHT
|
||||
{
|
||||
"continue": true,
|
||||
"suppressOutput": true
|
||||
}
|
||||
```
|
||||
|
||||
## References
|
||||
- Official Docs: https://docs.anthropic.com/en/docs/claude-code/hooks
|
||||
- Hook Examples: https://docs.anthropic.com/en/docs/claude-code/hooks-guide
|
||||
@@ -1,175 +0,0 @@
|
||||
# Claude Code Hooks Configuration Documentation
|
||||
## Source: Official Claude Code Docs v2025
|
||||
## Last Verified: 2025-08-31
|
||||
|
||||
## Hook Configuration Structure
|
||||
|
||||
### For Tool-Based Hooks (PreToolUse, PostToolUse)
|
||||
These hooks use the `matcher` field to match tool patterns:
|
||||
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"PreToolUse": [
|
||||
{
|
||||
"matcher": "ToolPattern", // Required for tool hooks
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "your-command-here",
|
||||
"timeout": 60000 // Optional, in milliseconds
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### For Non-Tool Hooks (PreCompact, SessionStart, etc.)
|
||||
These hooks DO NOT use matcher/pattern fields:
|
||||
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"PreCompact": [
|
||||
{
|
||||
// NO matcher or pattern field!
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "/path/to/script.js",
|
||||
"timeout": 180000
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Available Hook Events
|
||||
|
||||
### Tool-Related Hooks (use matcher)
|
||||
- **PreToolUse**: Before tool execution
|
||||
- **PostToolUse**: After tool execution
|
||||
|
||||
### System Event Hooks (no matcher)
|
||||
- **PreCompact**: Before conversation compaction
|
||||
- **SessionStart**: When session begins
|
||||
- **SessionEnd**: When session ends (not in official docs)
|
||||
- **UserPromptSubmit**: When user submits prompt
|
||||
- **Notification**: When Claude needs user input
|
||||
- **Stop**: When stop is requested
|
||||
- **SubagentStop**: When subagent stop is requested
|
||||
|
||||
## Hook Payload Structure
|
||||
|
||||
### Common Fields (all hooks)
|
||||
```json
|
||||
{
|
||||
"session_id": "string",
|
||||
"transcript_path": "string",
|
||||
"hook_event_name": "string",
|
||||
"cwd": "string" // Current working directory
|
||||
}
|
||||
```
|
||||
|
||||
### PreCompact Specific
|
||||
```json
|
||||
{
|
||||
"hook_event_name": "PreCompact",
|
||||
"trigger": "manual" | "auto",
|
||||
"custom_instructions": "string"
|
||||
}
|
||||
```
|
||||
|
||||
### SessionStart Specific
|
||||
```json
|
||||
{
|
||||
"hook_event_name": "SessionStart",
|
||||
"source": "startup" | "compact" | "vscode" | "web"
|
||||
}
|
||||
```
|
||||
|
||||
### PreToolUse/PostToolUse Specific
|
||||
```json
|
||||
{
|
||||
"tool_name": "string",
|
||||
"tool_input": { /* tool specific */ },
|
||||
"tool_response": { /* PostToolUse only */ }
|
||||
}
|
||||
```
|
||||
|
||||
## Common Configuration Mistakes
|
||||
|
||||
### \u274c INCORRECT: Using 'pattern' for non-tool hooks
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"PreCompact": [{
|
||||
"pattern": "*", // \u274c WRONG - non-tool hooks don't use this
|
||||
"hooks": [...]
|
||||
}]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### \u2705 CORRECT: No matcher for non-tool hooks
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"PreCompact": [{
|
||||
// No pattern or matcher field
|
||||
"hooks": [...]
|
||||
}]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### \u274c INCORRECT: Wrong matcher field name
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"PreToolUse": [{
|
||||
"pattern": "Bash", // \u274c WRONG field name
|
||||
"hooks": [...]
|
||||
}]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### \u2705 CORRECT: Using 'matcher' for tool hooks
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"PreToolUse": [{
|
||||
"matcher": "Bash", // \u2705 Correct field name
|
||||
"hooks": [...]
|
||||
}]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Matcher Patterns for Tool Hooks
|
||||
|
||||
- **Exact match**: `"Bash"` - matches only Bash tool
|
||||
- **Multiple tools**: `"Edit|MultiEdit|Write"` - matches any of these
|
||||
- **MCP tools**: `"mcp__memory__.*"` - matches all memory server tools
|
||||
- **All tools**: `"*"` - matches everything
|
||||
|
||||
## Environment Variables
|
||||
|
||||
Hooks have access to:
|
||||
- `$CLAUDE_PROJECT_DIR` - Project root directory
|
||||
|
||||
## Settings File Locations
|
||||
|
||||
1. **User settings**: `~/.claude/settings.json`
|
||||
2. **Project settings**: `./.claude/settings.json`
|
||||
3. **Local settings**: `./.claude/settings.local.json`
|
||||
4. **Managed settings**: `/Library/Application Support/ClaudeCode/managed-settings.json`
|
||||
|
||||
## References
|
||||
- Official Docs: https://docs.anthropic.com/en/docs/claude-code/hooks
|
||||
- Hook Guide: https://docs.anthropic.com/en/docs/claude-code/hooks-guide
|
||||
@@ -1,133 +0,0 @@
|
||||
# MCP Configuration Documentation
|
||||
## Source: Official Claude Code Docs v2025
|
||||
## Last Verified: 2025-08-31
|
||||
|
||||
## MCP Configuration File Locations
|
||||
|
||||
### User Scope
|
||||
- **File**: `~/.claude.json`
|
||||
- **Purpose**: User-wide MCP servers available across all projects
|
||||
- **Persistence**: Persists across projects
|
||||
- **Example Path**: `/Users/username/.claude.json`
|
||||
|
||||
### Project Scope
|
||||
- **File**: `./.mcp.json`
|
||||
- **Purpose**: Project-specific servers for team collaboration
|
||||
- **Persistence**: Checked into version control
|
||||
- **Example Path**: `/path/to/project/.mcp.json`
|
||||
|
||||
### Local Scope
|
||||
- **Status**: Not officially documented
|
||||
- **Implementation**: Currently uses `~/.claude.json` (may need revision)
|
||||
|
||||
## Configuration Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"server-name": {
|
||||
"command": "command-to-run",
|
||||
"args": ["arg1", "arg2"],
|
||||
"env": {
|
||||
"ENV_VAR": "value"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Example Configurations
|
||||
|
||||
### Memory Server (stdio)
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"claude-mem": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-memory"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### HTTP Server
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"api-server": {
|
||||
"type": "sse",
|
||||
"url": "${API_BASE_URL:-https://api.example.com}/mcp",
|
||||
"headers": {
|
||||
"Authorization": "Bearer ${API_KEY}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Environment Variable Expansion
|
||||
|
||||
MCP configs support environment variable expansion:
|
||||
- `${VAR}` - Direct expansion
|
||||
- `${VAR:-default}` - With fallback value
|
||||
|
||||
Applicable fields:
|
||||
- `command`
|
||||
- `args`
|
||||
- `env`
|
||||
- `url`
|
||||
- `headers`
|
||||
|
||||
## CLI Commands
|
||||
|
||||
```bash
|
||||
# Add a server
|
||||
claude mcp add <name> <command> [args...]
|
||||
|
||||
# Add with scope
|
||||
claude mcp add <name> --scope project /path/to/server
|
||||
claude mcp add <name> --scope user /path/to/server
|
||||
|
||||
# List servers
|
||||
claude mcp list
|
||||
|
||||
# Get server details
|
||||
claude mcp get <name>
|
||||
|
||||
# Remove server
|
||||
claude mcp remove <name>
|
||||
|
||||
# Check status (within Claude Code)
|
||||
/mcp
|
||||
```
|
||||
|
||||
## Tool Naming Convention
|
||||
|
||||
MCP tools follow the pattern: `mcp__<serverName>__<toolName>`
|
||||
|
||||
Example:
|
||||
- Server: `claude-mem`
|
||||
- Tool: `create_entities`
|
||||
- Full name: `mcp__claude_mem__create_entities`
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. **Tool Permissions**: Must explicitly allow MCP tools via `--allowedTools`
|
||||
2. **Server Trust**: Only use MCP servers from trusted sources
|
||||
3. **Credential Management**: Use environment variables for sensitive data
|
||||
4. **Audit Trail**: MCP operations can be monitored via hooks
|
||||
|
||||
## Common Issues
|
||||
|
||||
### Issue: MCP server not connecting
|
||||
**Solution**: Check that the command and args are correct, and npx is in PATH
|
||||
|
||||
### Issue: Tools not available
|
||||
**Solution**: Ensure server is in allowed list and properly configured
|
||||
|
||||
### Issue: Configuration not loading
|
||||
**Solution**: Verify JSON syntax and file location
|
||||
|
||||
## References
|
||||
- Official Docs: https://docs.anthropic.com/en/docs/claude-code/mcp
|
||||
- MCP Protocol: https://modelcontextprotocol.io/
|
||||
@@ -1,82 +0,0 @@
|
||||
# SessionStart Hook Documentation
|
||||
|
||||
## Official Documentation Reference
|
||||
- **Source**: https://docs.anthropic.com/en/docs/claude-code/hooks#sessionstart
|
||||
- **Last Verified**: 2025-08-31
|
||||
- **Version**: Claude Code v2025
|
||||
|
||||
## Hook Payload Structure
|
||||
|
||||
The SessionStart hook receives the following JSON payload via stdin:
|
||||
|
||||
```json
|
||||
{
|
||||
"session_id": "string",
|
||||
"transcript_path": "string",
|
||||
"hook_event_name": "SessionStart",
|
||||
"source": "startup" | "compact" | "vscode" | "web"
|
||||
}
|
||||
```
|
||||
|
||||
### Field Descriptions
|
||||
|
||||
- **session_id**: Unique identifier for the Claude Code session
|
||||
- **transcript_path**: Path to the conversation transcript JSONL file
|
||||
- **hook_event_name**: Always "SessionStart" for this hook
|
||||
- **source**: Indicates how the session was initiated:
|
||||
- `"startup"`: New session started normally (should load context)
|
||||
- `"compact"`: Session started after compaction (may skip context)
|
||||
- `"vscode"`: Session initiated from VS Code extension
|
||||
- `"web"`: Session initiated from web interface
|
||||
|
||||
## Response Format
|
||||
|
||||
The hook should output JSON in the following format to add context:
|
||||
|
||||
```json
|
||||
{
|
||||
"hookSpecificOutput": {
|
||||
"hookEventName": "SessionStart",
|
||||
"additionalContext": "string"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Response Fields
|
||||
|
||||
- **hookSpecificOutput**: Container for hook-specific output
|
||||
- **hookEventName**: Must be "SessionStart"
|
||||
- **additionalContext**: String content to add to the session context
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
### Context Loading Strategy
|
||||
|
||||
The hook should determine whether to load context based on the `source` field:
|
||||
|
||||
1. **For "startup" source**: Load full context from memory
|
||||
2. **For "compact" source**: Skip or load minimal context (session continuing after compaction)
|
||||
3. **For "vscode"/"web" sources**: Load context as appropriate
|
||||
|
||||
### Error Handling
|
||||
|
||||
- If context loading fails, exit silently (exit code 0)
|
||||
- Do not break the session start with errors
|
||||
- Log errors to separate log file if needed
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
### Incorrect Field Check (FIXED)
|
||||
**Wrong**: Checking `payload.reason === 'continue'`
|
||||
**Correct**: Checking `payload.source === 'compact'`
|
||||
|
||||
The payload does not have a `reason` field. The `source` field indicates the session initiation context.
|
||||
|
||||
## Code Location
|
||||
- **File**: `/Users/alexnewman/Scripts/claude-mem/hooks/session-start.js`
|
||||
- **Line**: 53-66 (field check and documentation)
|
||||
|
||||
## Cross-References
|
||||
- General Hooks Documentation: [docs/claude-code/hooks.md](./hooks.md)
|
||||
- Hook Response Formats: [docs/claude-code/hook-responses.md](./hook-responses.md)
|
||||
- MCP Configuration: [docs/claude-code/mcp-configuration.md](./mcp-configuration.md)
|
||||
@@ -1,33 +0,0 @@
|
||||
<session-start-hook>
|
||||
|
||||
🧠 What's new: Thursday, September 4, 2025 at 06:13 PM EDT
|
||||
====================================================================
|
||||
Established Claude memory management infrastructure with compression tools for session archiving. Optimized session summary generation with compact command overviews for improved retrieval efficiency.
|
||||
|
||||
📚 Recent Context
|
||||
====================================================================
|
||||
👀 in claude_mem_source_d096f650-76a3-4163-9b7a-19f36a13c648_{number}:
|
||||
|
||||
1. Removed maxTurns:1 limitation that prevented Claude from completing multi-step compression with tool calls
|
||||
— maxTurns, Claude SDK, compression fix, MCP tools
|
||||
|
||||
3. Deployed steve-krug-ux agent to redesign prompt from pipe-separated to JSON with XMLResponse tags
|
||||
— steve-krug-ux, JSON format, prompt redesign
|
||||
|
||||
5. Expanded TranscriptCompressor to use all 13 claude-mem MCP tools for intelligent compression
|
||||
— allowedTools, MCP tools expansion, TranscriptCompressor
|
||||
|
||||
6. Traced index population failure through multiple system layers to identify root cause
|
||||
— debugging, ContextTemplates, claude-mem-index, trace
|
||||
|
||||
======================================================================
|
||||
👀 in claude_mem_source_d096f650-76a3-4163-9b7a-19f36a13c648_{number}:
|
||||
|
||||
1. Implemented Claude memory management and compression tools system
|
||||
— memory management, compression, archiving, session context
|
||||
|
||||
2. Generated compact command overview for session summaries
|
||||
— command overview, session summaries, memory retrieval
|
||||
|
||||
======================================================================
|
||||
</session-start-hook>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,286 +0,0 @@
|
||||
# MCP TypeScript SDK Server Implementation Guide
|
||||
|
||||
## Documentation Source
|
||||
- **SDK Version**: @modelcontextprotocol/sdk v1.0.0
|
||||
- **Last Verified**: 2025-09-01
|
||||
- **Official Docs**: https://github.com/modelcontextprotocol/typescript-sdk
|
||||
|
||||
## Server Creation Patterns
|
||||
|
||||
### Low-Level Server vs McpServer
|
||||
|
||||
The SDK provides two approaches for creating MCP servers:
|
||||
|
||||
1. **Low-Level Server Class** (Used in claude-mem)
|
||||
- Direct control over request handling
|
||||
- Manual registration with `setRequestHandler`
|
||||
- More flexibility for custom routing logic
|
||||
|
||||
```typescript
|
||||
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
||||
|
||||
const server = new Server(
|
||||
{
|
||||
name: 'server-name',
|
||||
version: '1.0.0'
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
tools: {} // Declare tool capability
|
||||
}
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
2. **High-Level McpServer Class** (Alternative approach)
|
||||
- Simplified API with `registerTool`, `registerResource`, `registerPrompt`
|
||||
- Automatic routing and validation
|
||||
- Less boilerplate code
|
||||
|
||||
```typescript
|
||||
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||
|
||||
const server = new McpServer({
|
||||
name: 'server-name',
|
||||
version: '1.0.0'
|
||||
});
|
||||
```
|
||||
|
||||
## Tool Handler Registration
|
||||
|
||||
### Pattern 1: Single Handler with CallToolRequestSchema (claude-mem approach)
|
||||
|
||||
```typescript
|
||||
import { CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
||||
|
||||
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
const { name, arguments: args } = request.params;
|
||||
|
||||
// Validate arguments exist
|
||||
if (!args) {
|
||||
throw new Error(`No arguments provided for tool: ${name}`);
|
||||
}
|
||||
|
||||
// Route to specific tool implementation
|
||||
switch (name) {
|
||||
case 'tool-name':
|
||||
// Tool implementation
|
||||
return {
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: 'Result'
|
||||
}]
|
||||
};
|
||||
default:
|
||||
throw new Error(`Unknown tool: ${name}`);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Pattern 2: Individual Tool Registration (McpServer approach)
|
||||
|
||||
```typescript
|
||||
server.registerTool(
|
||||
'tool-name',
|
||||
{
|
||||
title: 'Tool Title',
|
||||
description: 'Tool description',
|
||||
inputSchema: { param: z.string() }
|
||||
},
|
||||
async ({ param }) => ({
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: `Result for ${param}`
|
||||
}]
|
||||
})
|
||||
);
|
||||
```
|
||||
|
||||
## Stdio Transport Usage
|
||||
|
||||
### Standard Pattern for CLI-based Servers
|
||||
|
||||
```typescript
|
||||
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||
|
||||
async function main() {
|
||||
// 1. Initialize backend services first
|
||||
await initializeBackend();
|
||||
|
||||
// 2. Create transport
|
||||
const transport = new StdioServerTransport();
|
||||
|
||||
// 3. Connect server to transport
|
||||
await server.connect(transport);
|
||||
|
||||
// 4. Log to stderr (stdout is for protocol)
|
||||
console.error('Server started on stdio');
|
||||
}
|
||||
```
|
||||
|
||||
### Key Points:
|
||||
- **Stdin**: Receives MCP protocol messages
|
||||
- **Stdout**: Sends MCP protocol responses
|
||||
- **Stderr**: Used for logging and diagnostics
|
||||
|
||||
## Error Handling Patterns
|
||||
|
||||
### Tool Error Response
|
||||
|
||||
```typescript
|
||||
try {
|
||||
// Tool implementation
|
||||
return {
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: 'Success result'
|
||||
}]
|
||||
};
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
|
||||
// Log to stderr for debugging
|
||||
console.error(`[Error] Tool: ${name}, Error: ${errorMessage}`);
|
||||
|
||||
// Return error response with isError flag
|
||||
return {
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: `Error: ${errorMessage}`
|
||||
}],
|
||||
isError: true // Important: Indicates tool failure
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Startup Error Handling
|
||||
|
||||
```typescript
|
||||
main().catch((error) => {
|
||||
console.error('Startup error:', error);
|
||||
process.exit(1); // Exit with error code
|
||||
});
|
||||
```
|
||||
|
||||
## Response Formatting
|
||||
|
||||
### Success Response Structure
|
||||
|
||||
```typescript
|
||||
{
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: 'Response text'
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Error Response Structure
|
||||
|
||||
```typescript
|
||||
{
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: 'Error: Description'
|
||||
}
|
||||
],
|
||||
isError: true
|
||||
}
|
||||
```
|
||||
|
||||
### Resource Link Response
|
||||
|
||||
```typescript
|
||||
{
|
||||
content: [
|
||||
{
|
||||
type: 'resource_link',
|
||||
uri: 'file:///path/to/resource',
|
||||
name: 'Resource Name',
|
||||
mimeType: 'text/plain',
|
||||
description: 'Resource description'
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Lifecycle Management
|
||||
|
||||
### Initialization Pattern
|
||||
|
||||
```typescript
|
||||
async function initializeServer(): Promise<void> {
|
||||
try {
|
||||
// Initialize backend connections
|
||||
await backend.connect();
|
||||
console.error('Backend initialized');
|
||||
} catch (error) {
|
||||
console.error('Initialization failed:', error);
|
||||
throw error; // Prevent server startup
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Shutdown Pattern
|
||||
|
||||
```typescript
|
||||
function setupShutdownHandlers(): void {
|
||||
const handleShutdown = async (signal: string) => {
|
||||
console.error(`Received ${signal}, shutting down...`);
|
||||
|
||||
try {
|
||||
await backend.disconnect();
|
||||
process.exit(0); // Clean exit
|
||||
} catch (error) {
|
||||
console.error('Shutdown error:', error);
|
||||
process.exit(1); // Error exit
|
||||
}
|
||||
};
|
||||
|
||||
process.on('SIGINT', () => handleShutdown('SIGINT'));
|
||||
process.on('SIGTERM', () => handleShutdown('SIGTERM'));
|
||||
|
||||
// Handle unexpected errors
|
||||
process.on('uncaughtException', async (error) => {
|
||||
console.error('Uncaught exception:', error);
|
||||
await backend.disconnect();
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices Summary
|
||||
|
||||
1. **Server Creation**
|
||||
- Use low-level Server for custom routing
|
||||
- Use McpServer for standard implementations
|
||||
|
||||
2. **Transport Usage**
|
||||
- Initialize backends before connecting transport
|
||||
- Use StdioServerTransport for CLI tools
|
||||
- Log to stderr, not stdout
|
||||
|
||||
3. **Error Handling**
|
||||
- Always validate tool arguments
|
||||
- Include isError flag in error responses
|
||||
- Log errors to stderr with context
|
||||
|
||||
4. **Response Format**
|
||||
- Always return content array
|
||||
- Use consistent type/text structure
|
||||
- Include isError for failures
|
||||
|
||||
5. **Lifecycle**
|
||||
- Clean initialization sequence
|
||||
- Graceful shutdown handlers
|
||||
- Proper exit codes (0 for success, 1 for error)
|
||||
|
||||
## References
|
||||
|
||||
- [MCP TypeScript SDK README](https://github.com/modelcontextprotocol/typescript-sdk)
|
||||
- [Low-Level Server Pattern](https://github.com/modelcontextprotocol/typescript-sdk#low-level-server-implementation)
|
||||
- [Stdio Transport Example](https://github.com/modelcontextprotocol/typescript-sdk#stdio-transport)
|
||||
- [Error Handling Examples](https://github.com/modelcontextprotocol/typescript-sdk#sqlite-explorer)
|
||||
@@ -1,345 +0,0 @@
|
||||
# MCP TypeScript SDK Stdio Transport Guide
|
||||
|
||||
## Documentation Source
|
||||
- **SDK Version**: @modelcontextprotocol/sdk v1.0.0
|
||||
- **Last Verified**: 2025-09-01
|
||||
- **Official Docs**: https://github.com/modelcontextprotocol/typescript-sdk
|
||||
|
||||
## Stdio Transport Overview
|
||||
|
||||
The StdioServerTransport enables MCP servers to communicate via standard input/output streams, making them ideal for CLI tools and direct integrations with Claude Code.
|
||||
|
||||
## Communication Channels
|
||||
|
||||
### Stream Usage
|
||||
- **stdin**: Receives MCP protocol messages (JSON-RPC)
|
||||
- **stdout**: Sends MCP protocol responses (JSON-RPC)
|
||||
- **stderr**: Logging and diagnostic output
|
||||
|
||||
### Important Rules
|
||||
1. **Never write non-protocol data to stdout** - This will break the protocol
|
||||
2. **Always use console.error() for logging** - Goes to stderr
|
||||
3. **Handle binary data carefully** - Protocol is text-based JSON
|
||||
|
||||
## Implementation Patterns
|
||||
|
||||
### Basic Stdio Server Setup
|
||||
|
||||
```typescript
|
||||
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
||||
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||
|
||||
// Create server
|
||||
const server = new Server(
|
||||
{ name: 'my-server', version: '1.0.0' },
|
||||
{ capabilities: { tools: {} } }
|
||||
);
|
||||
|
||||
// Create and connect transport
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
|
||||
// Server is now listening on stdin/stdout
|
||||
console.error('Server started'); // Note: console.error for logging
|
||||
```
|
||||
|
||||
### With McpServer (High-Level API)
|
||||
|
||||
```typescript
|
||||
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||
|
||||
const server = new McpServer({
|
||||
name: 'my-server',
|
||||
version: '1.0.0'
|
||||
});
|
||||
|
||||
// Register tools, resources, prompts...
|
||||
server.registerTool(...);
|
||||
|
||||
// Connect to stdio
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
```
|
||||
|
||||
## CLI Entry Point Pattern
|
||||
|
||||
### Proper Module Detection (ES Modules)
|
||||
|
||||
```typescript
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname } from 'path';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
// Only run if executed directly
|
||||
if (process.argv[1] === __filename ||
|
||||
process.argv[1].endsWith('server.js')) {
|
||||
main().catch((error) => {
|
||||
console.error('Startup error:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Main Function Pattern
|
||||
|
||||
```typescript
|
||||
async function main(): Promise<void> {
|
||||
try {
|
||||
// 1. Initialize dependencies
|
||||
await initializeDatabase();
|
||||
|
||||
// 2. Create server
|
||||
const server = createServer();
|
||||
|
||||
// 3. Create transport
|
||||
const transport = new StdioServerTransport();
|
||||
|
||||
// 4. Connect
|
||||
await server.connect(transport);
|
||||
|
||||
// 5. Setup shutdown handlers
|
||||
setupShutdownHandlers();
|
||||
|
||||
// 6. Log readiness to stderr
|
||||
console.error('MCP server ready on stdio');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to start server:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Shutdown Handling
|
||||
|
||||
### Graceful Shutdown Pattern
|
||||
|
||||
```typescript
|
||||
function setupShutdownHandlers(): void {
|
||||
const shutdown = async (signal: string) => {
|
||||
console.error(`\nReceived ${signal}, shutting down...`);
|
||||
|
||||
try {
|
||||
// Clean up resources
|
||||
await cleanupResources();
|
||||
|
||||
// Note: Transport cleanup is handled automatically
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error('Shutdown error:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle termination signals
|
||||
process.on('SIGINT', () => shutdown('SIGINT')); // Ctrl+C
|
||||
process.on('SIGTERM', () => shutdown('SIGTERM')); // Kill
|
||||
process.on('SIGHUP', () => shutdown('SIGHUP')); // Terminal closed
|
||||
|
||||
// Handle errors
|
||||
process.on('uncaughtException', (error) => {
|
||||
console.error('Uncaught exception:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
process.on('unhandledRejection', (reason) => {
|
||||
console.error('Unhandled rejection:', reason);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## Logging Best Practices
|
||||
|
||||
### Do's and Don'ts
|
||||
|
||||
```typescript
|
||||
// ✅ DO: Log to stderr
|
||||
console.error('Server initialized');
|
||||
console.error('Processing request:', requestId);
|
||||
console.error('Debug info:', { data });
|
||||
|
||||
// ❌ DON'T: Log to stdout
|
||||
console.log('This breaks the protocol!'); // NEVER DO THIS
|
||||
|
||||
// ✅ DO: Use structured logging to stderr
|
||||
const log = (level: string, message: string, data?: any) => {
|
||||
console.error(JSON.stringify({
|
||||
timestamp: new Date().toISOString(),
|
||||
level,
|
||||
message,
|
||||
...data
|
||||
}));
|
||||
};
|
||||
|
||||
log('info', 'Server started', { port: 'stdio' });
|
||||
```
|
||||
|
||||
### Debug Mode Pattern
|
||||
|
||||
```typescript
|
||||
const DEBUG = process.env.DEBUG === 'true';
|
||||
|
||||
const debug = (message: string, ...args: any[]) => {
|
||||
if (DEBUG) {
|
||||
console.error(`[DEBUG] ${message}`, ...args);
|
||||
}
|
||||
};
|
||||
|
||||
// Usage
|
||||
debug('Request received:', request);
|
||||
```
|
||||
|
||||
## Testing Stdio Servers
|
||||
|
||||
### Manual Testing
|
||||
|
||||
```bash
|
||||
# Start server and interact manually
|
||||
node dist/server.js
|
||||
|
||||
# With debug logging
|
||||
DEBUG=true node dist/server.js
|
||||
|
||||
# Pipe test input
|
||||
echo '{"jsonrpc":"2.0","method":"initialize","params":{},"id":1}' | node dist/server.js
|
||||
```
|
||||
|
||||
### Automated Testing Pattern
|
||||
|
||||
```typescript
|
||||
import { spawn } from 'child_process';
|
||||
|
||||
function testServer() {
|
||||
const server = spawn('node', ['dist/server.js']);
|
||||
|
||||
// Capture stderr for logs
|
||||
server.stderr.on('data', (data) => {
|
||||
console.log('Server log:', data.toString());
|
||||
});
|
||||
|
||||
// Capture stdout for protocol
|
||||
let response = '';
|
||||
server.stdout.on('data', (data) => {
|
||||
response += data.toString();
|
||||
// Parse and validate response
|
||||
});
|
||||
|
||||
// Send test request
|
||||
server.stdin.write(JSON.stringify({
|
||||
jsonrpc: '2.0',
|
||||
method: 'initialize',
|
||||
params: {},
|
||||
id: 1
|
||||
}));
|
||||
server.stdin.end();
|
||||
}
|
||||
```
|
||||
|
||||
## Common Issues and Solutions
|
||||
|
||||
### Issue 1: Protocol Corruption
|
||||
|
||||
**Problem**: Random text in stdout breaks communication
|
||||
**Solution**: Always use console.error() for logging
|
||||
|
||||
```typescript
|
||||
// Wrong
|
||||
console.log('Debug:', data); // Breaks protocol
|
||||
|
||||
// Right
|
||||
console.error('Debug:', data); // Safe for debugging
|
||||
```
|
||||
|
||||
### Issue 2: Server Not Responding
|
||||
|
||||
**Problem**: Server starts but doesn't respond to requests
|
||||
**Solution**: Ensure transport is connected
|
||||
|
||||
```typescript
|
||||
// Check connection is awaited
|
||||
await server.connect(transport); // Must await!
|
||||
console.error('Transport connected');
|
||||
```
|
||||
|
||||
### Issue 3: Premature Exit
|
||||
|
||||
**Problem**: Server exits immediately
|
||||
**Solution**: Don't close stdin/stdout
|
||||
|
||||
```typescript
|
||||
// Wrong
|
||||
process.stdin.end(); // Don't do this
|
||||
|
||||
// Right
|
||||
// Let the transport manage streams
|
||||
```
|
||||
|
||||
## Integration with Claude Code
|
||||
|
||||
### Configuration in .claude.json
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"my-server": {
|
||||
"command": "node",
|
||||
"args": ["/path/to/dist/server.js"],
|
||||
"env": {
|
||||
"DEBUG": "false"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Best Practices for Claude Code Integration
|
||||
|
||||
1. **Startup Messages**: Log clear startup messages to stderr
|
||||
2. **Error Messages**: Provide actionable error messages
|
||||
3. **Ready Signal**: Log when server is ready to accept requests
|
||||
4. **Version Info**: Include version in startup logs
|
||||
|
||||
```typescript
|
||||
console.error(`Starting ${serverName} v${version}`);
|
||||
console.error('Initializing...');
|
||||
// ... initialization ...
|
||||
console.error(`${serverName} ready on stdio`);
|
||||
```
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### Buffering and Streaming
|
||||
|
||||
```typescript
|
||||
// For large responses, consider streaming
|
||||
import { Transform } from 'stream';
|
||||
|
||||
class ResponseStream extends Transform {
|
||||
_transform(chunk: any, encoding: string, callback: Function) {
|
||||
// Process chunk
|
||||
this.push(JSON.stringify(chunk));
|
||||
callback();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Memory Management
|
||||
|
||||
```typescript
|
||||
// Clear large objects after use
|
||||
let largeData = await processData();
|
||||
// Use data...
|
||||
largeData = null; // Allow GC
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
- [StdioServerTransport Docs](https://github.com/modelcontextprotocol/typescript-sdk#stdio-transport)
|
||||
- [Server Examples](https://github.com/modelcontextprotocol/typescript-sdk/tree/main/src/examples/server)
|
||||
- [MCP Protocol Specification](https://modelcontextprotocol.io/docs)
|
||||
Executable
+150
@@ -0,0 +1,150 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Post Tool Use Hook - Streaming SDK Version
|
||||
*
|
||||
* Feeds tool responses to the streaming SDK session for real-time processing.
|
||||
* SDK decides what to store and calls bash commands directly.
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { query } from '@anthropic-ai/claude-agent-sdk';
|
||||
import { renderToolMessage, HOOK_CONFIG } from './shared/hook-prompt-renderer.js';
|
||||
import { getProjectName } from './shared/path-resolver.js';
|
||||
import { initializeDatabase, getActiveStreamingSessionsForProject } from './shared/hook-helpers.js';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const HOOKS_LOG = path.join(process.env.HOME || '', '.claude-mem', 'logs', 'hooks.log');
|
||||
|
||||
function debugLog(message, data = {}) {
|
||||
if (process.env.CLAUDE_MEM_DEBUG === 'true') {
|
||||
const timestamp = new Date().toISOString();
|
||||
const logLine = `[${timestamp}] HOOK DEBUG: ${message} ${JSON.stringify(data)}\n`;
|
||||
try {
|
||||
fs.appendFileSync(HOOKS_LOG, logLine);
|
||||
process.stderr.write(logLine);
|
||||
} catch (error) {
|
||||
// Silent fail on log errors
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Removed: buildStreamingToolMessage function
|
||||
// Now using centralized config from hook-prompt-renderer.js
|
||||
|
||||
// =============================================================================
|
||||
// MAIN
|
||||
// =============================================================================
|
||||
|
||||
let input = '';
|
||||
process.stdin.setEncoding('utf8');
|
||||
process.stdin.on('data', (chunk) => { input += chunk; });
|
||||
|
||||
process.stdin.on('end', async () => {
|
||||
let payload;
|
||||
try {
|
||||
payload = input ? JSON.parse(input) : {};
|
||||
} catch (error) {
|
||||
debugLog('PostToolUse: JSON parse error', { error: error.message });
|
||||
console.log(JSON.stringify({ continue: true, suppressOutput: true }));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const { tool_name, tool_response, prompt, cwd, timestamp } = payload;
|
||||
const project = cwd ? getProjectName(cwd) : 'unknown';
|
||||
|
||||
// Return immediately - process async in background (don't block next tool)
|
||||
console.log(JSON.stringify({ async: true, asyncTimeout: 180000 }));
|
||||
|
||||
try {
|
||||
// Load SDK session info from database
|
||||
const db = initializeDatabase();
|
||||
|
||||
const sessions = getActiveStreamingSessionsForProject(db, project);
|
||||
if (!sessions || sessions.length === 0) {
|
||||
debugLog('PostToolUse: No streaming session found', { project });
|
||||
db.close();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const sessionData = sessions[0];
|
||||
const sdkSessionId = sessionData.sdk_session_id;
|
||||
|
||||
// Convert tool response to string
|
||||
const toolResponseStr = typeof tool_response === 'string'
|
||||
? tool_response
|
||||
: JSON.stringify(tool_response);
|
||||
|
||||
// Build message for SDK using centralized config
|
||||
const message = renderToolMessage({
|
||||
toolName: tool_name,
|
||||
toolResponse: toolResponseStr,
|
||||
userPrompt: prompt || '',
|
||||
timestamp: timestamp || new Date().toISOString()
|
||||
});
|
||||
|
||||
// Send to SDK and wait for processing to complete using centralized config
|
||||
const response = query({
|
||||
prompt: message,
|
||||
options: {
|
||||
model: HOOK_CONFIG.sdk.model,
|
||||
resume: sdkSessionId,
|
||||
allowedTools: HOOK_CONFIG.sdk.allowedTools,
|
||||
maxTokens: HOOK_CONFIG.sdk.maxTokensTool,
|
||||
cwd // Must match where transcript was created
|
||||
}
|
||||
});
|
||||
|
||||
// Consume the stream to let SDK fully process
|
||||
for await (const msg of response) {
|
||||
debugLog('PostToolUse: SDK message', { type: msg.type, subtype: msg.subtype });
|
||||
|
||||
// SDK messages are structured differently than we expected
|
||||
// - type: 'assistant' contains the assistant's response with content blocks
|
||||
// - Content blocks can be text or tool_use
|
||||
// - type: 'user' contains tool results
|
||||
// - type: 'result' is the final summary
|
||||
|
||||
if (msg.type === 'assistant' && msg.message?.content) {
|
||||
for (const block of msg.message.content) {
|
||||
if (block.type === 'text') {
|
||||
debugLog('PostToolUse: SDK text', { text: block.text?.slice(0, 200) });
|
||||
} else if (block.type === 'tool_use') {
|
||||
debugLog('PostToolUse: SDK tool_use', {
|
||||
tool: block.name,
|
||||
input: JSON.stringify(block.input).slice(0, 200)
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (msg.type === 'user' && msg.message?.content) {
|
||||
for (const block of msg.message.content) {
|
||||
if (block.type === 'tool_result') {
|
||||
debugLog('PostToolUse: SDK tool_result', {
|
||||
tool_use_id: block.tool_use_id,
|
||||
content: typeof block.content === 'string' ? block.content.slice(0, 300) : JSON.stringify(block.content).slice(0, 300)
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (msg.type === 'result') {
|
||||
debugLog('PostToolUse: SDK result', {
|
||||
subtype: msg.subtype,
|
||||
is_error: msg.is_error
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
debugLog('PostToolUse: SDK finished processing', { tool_name, sdkSessionId });
|
||||
|
||||
// Close database connection
|
||||
db.close();
|
||||
} catch (error) {
|
||||
debugLog('PostToolUse: Error sending to SDK', { error: error.message });
|
||||
}
|
||||
|
||||
// Exit cleanly after async processing completes
|
||||
process.exit(0);
|
||||
});
|
||||
Executable
+57
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Session Start Hook (SDK Version)
|
||||
*
|
||||
* Calls the CLI to load relevant context from ChromaDB at session start.
|
||||
*/
|
||||
|
||||
import { createHookResponse, debugLog } from './shared/hook-helpers.js';
|
||||
|
||||
// Read stdin
|
||||
let input = '';
|
||||
process.stdin.setEncoding('utf8');
|
||||
process.stdin.on('data', (chunk) => {
|
||||
input += chunk;
|
||||
});
|
||||
|
||||
process.stdin.on('end', async () => {
|
||||
const payload = input ? JSON.parse(input) : {};
|
||||
|
||||
debugLog('SessionStart hook invoked (SDK version)', { cwd: payload.cwd });
|
||||
|
||||
const { cwd, source } = payload;
|
||||
|
||||
// Run on startup or /clear
|
||||
if (source !== 'startup' && source !== 'clear') {
|
||||
const response = createHookResponse('SessionStart', true);
|
||||
console.log(JSON.stringify(response));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
try {
|
||||
// Call the CLI to load context
|
||||
const { executeCliCommand } = await import('./shared/hook-helpers.js');
|
||||
|
||||
const result = await executeCliCommand('claude-mem', ['load-context', '--format', 'session-start']);
|
||||
|
||||
if (result.success && result.stdout) {
|
||||
// Use the CLI output directly as context (it's already formatted)
|
||||
const response = createHookResponse('SessionStart', true, {
|
||||
context: result.stdout
|
||||
});
|
||||
console.log(JSON.stringify(response));
|
||||
process.exit(0);
|
||||
} else {
|
||||
// Return without context
|
||||
const response = createHookResponse('SessionStart', true);
|
||||
console.log(JSON.stringify(response));
|
||||
process.exit(0);
|
||||
}
|
||||
} catch (error) {
|
||||
// Continue without context on error
|
||||
const response = createHookResponse('SessionStart', true);
|
||||
console.log(JSON.stringify(response));
|
||||
process.exit(0);
|
||||
}
|
||||
});
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
/**
|
||||
* Hook Helper Functions
|
||||
*
|
||||
*
|
||||
* This module provides JavaScript wrappers around the TypeScript PromptOrchestrator
|
||||
* and HookTemplates system, making them accessible to the JavaScript hook scripts.
|
||||
*/
|
||||
@@ -10,6 +10,9 @@
|
||||
import { spawn } from 'child_process';
|
||||
import { join, dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import Database from 'better-sqlite3';
|
||||
import os from 'os';
|
||||
import fs from 'fs';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
@@ -39,33 +42,32 @@ export function createHookResponse(hookType, success, options = {}) {
|
||||
if (hookType === 'SessionStart') {
|
||||
if (success && options.context) {
|
||||
return {
|
||||
continue: true,
|
||||
suppressOutput: true,
|
||||
hookSpecificOutput: {
|
||||
hookEventName: 'SessionStart',
|
||||
additionalContext: options.context
|
||||
}
|
||||
};
|
||||
} else if (success) {
|
||||
return {
|
||||
continue: true,
|
||||
suppressOutput: true,
|
||||
hookSpecificOutput: {
|
||||
hookEventName: 'SessionStart',
|
||||
additionalContext: 'Starting fresh session - no previous context available'
|
||||
}
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
continue: true, // Continue even on context loading failure
|
||||
suppressOutput: true,
|
||||
hookSpecificOutput: {
|
||||
hookEventName: 'SessionStart',
|
||||
additionalContext: `Context loading encountered an issue: ${options.error || 'Unknown error'}. Starting without previous context.`
|
||||
}
|
||||
continue: true,
|
||||
suppressOutput: true
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (hookType === 'UserPromptSubmit' || hookType === 'PostToolUse') {
|
||||
return {
|
||||
continue: true,
|
||||
suppressOutput: true
|
||||
};
|
||||
}
|
||||
|
||||
if (hookType === 'Stop') {
|
||||
return {
|
||||
continue: true,
|
||||
suppressOutput: true
|
||||
};
|
||||
}
|
||||
|
||||
// Generic response for unknown hook types
|
||||
return {
|
||||
@@ -118,9 +120,10 @@ export function formatSessionStartContext(contextData) {
|
||||
*/
|
||||
export async function executeCliCommand(command, args = [], options = {}) {
|
||||
return new Promise((resolve) => {
|
||||
const { input, ...spawnOptions } = options;
|
||||
const process = spawn(command, args, {
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
...options
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
...spawnOptions
|
||||
});
|
||||
|
||||
let stdout = '';
|
||||
@@ -138,6 +141,13 @@ export async function executeCliCommand(command, args = [], options = {}) {
|
||||
});
|
||||
}
|
||||
|
||||
if (input && process.stdin) {
|
||||
process.stdin.write(input);
|
||||
process.stdin.end();
|
||||
} else if (process.stdin) {
|
||||
process.stdin.end();
|
||||
}
|
||||
|
||||
process.on('close', (code) => {
|
||||
resolve({
|
||||
stdout: stdout.trim(),
|
||||
@@ -227,4 +237,194 @@ export function debugLog(message, data = {}) {
|
||||
const timestamp = new Date().toISOString();
|
||||
console.error(`[${timestamp}] HOOK DEBUG: ${message}`, data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// DATABASE HELPERS (inline SQL to avoid 'claude-mem' import issues)
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Get the claude-mem data directory path
|
||||
*/
|
||||
function getDataDirectory() {
|
||||
return join(os.homedir(), '.claude-mem');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or create the database connection
|
||||
*/
|
||||
function getDatabase() {
|
||||
const dataDir = getDataDirectory();
|
||||
const dbPath = join(dataDir, 'claude-mem.db');
|
||||
|
||||
// Ensure directory exists
|
||||
if (!fs.existsSync(dataDir)) {
|
||||
fs.mkdirSync(dataDir, { recursive: true });
|
||||
}
|
||||
|
||||
const db = new Database(dbPath);
|
||||
|
||||
// Apply optimized SQLite settings
|
||||
db.pragma('journal_mode = WAL');
|
||||
db.pragma('synchronous = NORMAL');
|
||||
db.pragma('foreign_keys = ON');
|
||||
db.pragma('temp_store = memory');
|
||||
|
||||
return db;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the streaming_sessions table exists
|
||||
*/
|
||||
function ensureStreamingSessionsTable(db) {
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS streaming_sessions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
claude_session_id TEXT UNIQUE NOT NULL,
|
||||
sdk_session_id TEXT,
|
||||
project TEXT NOT NULL,
|
||||
title TEXT,
|
||||
subtitle TEXT,
|
||||
user_prompt TEXT,
|
||||
started_at TEXT NOT NULL,
|
||||
started_at_epoch INTEGER NOT NULL,
|
||||
updated_at TEXT,
|
||||
updated_at_epoch INTEGER,
|
||||
completed_at TEXT,
|
||||
completed_at_epoch INTEGER,
|
||||
status TEXT NOT NULL CHECK(status IN ('active', 'completed', 'failed'))
|
||||
)
|
||||
`);
|
||||
|
||||
// Create indices if they don't exist
|
||||
db.exec(`
|
||||
CREATE INDEX IF NOT EXISTS idx_streaming_sessions_claude_id
|
||||
ON streaming_sessions(claude_session_id)
|
||||
`);
|
||||
db.exec(`
|
||||
CREATE INDEX IF NOT EXISTS idx_streaming_sessions_sdk_id
|
||||
ON streaming_sessions(sdk_session_id)
|
||||
`);
|
||||
db.exec(`
|
||||
CREATE INDEX IF NOT EXISTS idx_streaming_sessions_project_status
|
||||
ON streaming_sessions(project, status)
|
||||
`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new streaming session record
|
||||
*/
|
||||
export function createStreamingSession(db, { claude_session_id, project, user_prompt, started_at }) {
|
||||
ensureStreamingSessionsTable(db);
|
||||
|
||||
const timestamp = started_at || new Date().toISOString();
|
||||
const epoch = new Date(timestamp).getTime();
|
||||
|
||||
const stmt = db.prepare(`
|
||||
INSERT INTO streaming_sessions (
|
||||
claude_session_id, project, user_prompt, started_at, started_at_epoch, status
|
||||
) VALUES (?, ?, ?, ?, ?, 'active')
|
||||
`);
|
||||
|
||||
const info = stmt.run(claude_session_id, project, user_prompt || null, timestamp, epoch);
|
||||
|
||||
return db.prepare('SELECT * FROM streaming_sessions WHERE id = ?').get(info.lastInsertRowid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a streaming session by internal ID
|
||||
*/
|
||||
export function updateStreamingSession(db, id, updates) {
|
||||
const timestamp = new Date().toISOString();
|
||||
const epoch = Date.now();
|
||||
|
||||
const parts = [];
|
||||
const values = [];
|
||||
|
||||
if (updates.sdk_session_id !== undefined) {
|
||||
parts.push('sdk_session_id = ?');
|
||||
values.push(updates.sdk_session_id);
|
||||
}
|
||||
if (updates.title !== undefined) {
|
||||
parts.push('title = ?');
|
||||
values.push(updates.title);
|
||||
}
|
||||
if (updates.subtitle !== undefined) {
|
||||
parts.push('subtitle = ?');
|
||||
values.push(updates.subtitle);
|
||||
}
|
||||
if (updates.status !== undefined) {
|
||||
parts.push('status = ?');
|
||||
values.push(updates.status);
|
||||
}
|
||||
if (updates.completed_at !== undefined) {
|
||||
const completedTimestamp = typeof updates.completed_at === 'string'
|
||||
? updates.completed_at
|
||||
: new Date(updates.completed_at).toISOString();
|
||||
const completedEpoch = new Date(completedTimestamp).getTime();
|
||||
parts.push('completed_at = ?', 'completed_at_epoch = ?');
|
||||
values.push(completedTimestamp, completedEpoch);
|
||||
}
|
||||
|
||||
// Always update the updated_at timestamp
|
||||
parts.push('updated_at = ?', 'updated_at_epoch = ?');
|
||||
values.push(timestamp, epoch);
|
||||
|
||||
values.push(id);
|
||||
|
||||
const stmt = db.prepare(`
|
||||
UPDATE streaming_sessions
|
||||
SET ${parts.join(', ')}
|
||||
WHERE id = ?
|
||||
`);
|
||||
|
||||
stmt.run(...values);
|
||||
|
||||
return db.prepare('SELECT * FROM streaming_sessions WHERE id = ?').get(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get active streaming sessions for a project
|
||||
*/
|
||||
export function getActiveStreamingSessionsForProject(db, project) {
|
||||
ensureStreamingSessionsTable(db);
|
||||
|
||||
const stmt = db.prepare(`
|
||||
SELECT * FROM streaming_sessions
|
||||
WHERE project = ? AND status = 'active'
|
||||
ORDER BY started_at_epoch DESC
|
||||
`);
|
||||
|
||||
return stmt.all(project);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a session as completed
|
||||
*/
|
||||
export function markStreamingSessionCompleted(db, id) {
|
||||
const timestamp = new Date().toISOString();
|
||||
const epoch = Date.now();
|
||||
|
||||
const stmt = db.prepare(`
|
||||
UPDATE streaming_sessions
|
||||
SET status = ?,
|
||||
completed_at = ?,
|
||||
completed_at_epoch = ?,
|
||||
updated_at = ?,
|
||||
updated_at_epoch = ?
|
||||
WHERE id = ?
|
||||
`);
|
||||
|
||||
stmt.run('completed', timestamp, epoch, timestamp, epoch, id);
|
||||
|
||||
return db.prepare('SELECT * FROM streaming_sessions WHERE id = ?').get(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize database with migrations and return connection
|
||||
*/
|
||||
export function initializeDatabase() {
|
||||
const db = getDatabase();
|
||||
ensureStreamingSessionsTable(db);
|
||||
return db;
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
// src/prompts/hook-prompts.config.ts
|
||||
var HOOK_CONFIG = {
|
||||
maxUserPromptLength: 200,
|
||||
maxToolResponseLength: 20000,
|
||||
sdk: {
|
||||
model: "claude-sonnet-4-5",
|
||||
allowedTools: ["Bash"],
|
||||
maxTokensSystem: 8192,
|
||||
maxTokensTool: 8192,
|
||||
maxTokensEnd: 2048
|
||||
}
|
||||
};
|
||||
var SYSTEM_PROMPT = `You are a semantic memory compressor for claude-mem. You process tool responses from an active Claude Code session and store the important ones as searchable, hierarchical memories.
|
||||
|
||||
# SESSION CONTEXT
|
||||
- Project: {{project}}
|
||||
- Session: {{sessionId}}
|
||||
- Date: {{date}}
|
||||
- User Request: "{{userPrompt}}"
|
||||
|
||||
# YOUR JOB
|
||||
|
||||
## FIRST: Generate Session Title
|
||||
|
||||
IMMEDIATELY generate a title and subtitle for this session based on the user request.
|
||||
|
||||
Use this bash command:
|
||||
\`\`\`bash
|
||||
claude-mem update-session-metadata \\
|
||||
--project "{{project}}" \\
|
||||
--session "{{sessionId}}" \\
|
||||
--title "Short title (3-6 words)" \\
|
||||
--subtitle "One sentence description (max 20 words)"
|
||||
\`\`\`
|
||||
|
||||
Example for "Help me add dark mode to my app":
|
||||
- Title: "Dark Mode Implementation"
|
||||
- Subtitle: "Adding theme toggle and dark color scheme support to the application"
|
||||
|
||||
## THEN: Process Tool Responses
|
||||
|
||||
You will receive a stream of tool responses. For each one:
|
||||
|
||||
1. ANALYZE: Does this contain information worth remembering?
|
||||
2. DECIDE: Should I store this or skip it?
|
||||
3. EXTRACT: What are the key semantic concepts?
|
||||
4. DECOMPOSE: Break into title + subtitle + atomic facts + narrative
|
||||
5. STORE: Use bash to save the hierarchical memory
|
||||
6. TRACK: Keep count of stored memories (001, 002, 003...)
|
||||
|
||||
# WHAT TO STORE
|
||||
|
||||
Store these:
|
||||
- File contents with logic, algorithms, or patterns
|
||||
- Search results revealing project structure
|
||||
- Build errors or test failures with context
|
||||
- Code revealing architecture or design decisions
|
||||
- Git diffs with significant changes
|
||||
- Command outputs showing system state
|
||||
|
||||
Skip these:
|
||||
- Simple status checks (git status with no changes)
|
||||
- Trivial edits (one-line config changes)
|
||||
- Repeated operations
|
||||
- Binary data or noise
|
||||
- Anything without semantic value
|
||||
|
||||
# HIERARCHICAL MEMORY FORMAT
|
||||
|
||||
Each memory has FOUR components:
|
||||
|
||||
## 1. TITLE (3-8 words)
|
||||
A scannable headline that captures the core action or topic.
|
||||
Examples:
|
||||
- "SDK Transcript Cleanup Implementation"
|
||||
- "Hook System Architecture Analysis"
|
||||
- "ChromaDB Migration Planning"
|
||||
|
||||
## 2. SUBTITLE (max 24 words)
|
||||
A concise, memorable summary that captures the essence of the change.
|
||||
Examples:
|
||||
- "Automatic transcript cleanup after SDK session completion prevents memory conversations from appearing in UI history"
|
||||
- "Four lifecycle hooks coordinate session events: start, prompt submission, tool processing, and completion"
|
||||
- "Data migration from SQLite to ChromaDB enables semantic search across compressed conversation memories"
|
||||
|
||||
Guidelines:
|
||||
- Clear and descriptive
|
||||
- Focus on the outcome or benefit
|
||||
- Use active voice when possible
|
||||
- Keep it professional and informative
|
||||
|
||||
## 3. ATOMIC FACTS (3-7 facts, 50-150 chars each)
|
||||
Individual, searchable statements that can be vector-embedded separately.
|
||||
Each fact is ONE specific piece of information.
|
||||
|
||||
Examples:
|
||||
- "stop-streaming.js: Auto-deletes SDK transcripts after completion"
|
||||
- "Path format: ~/.claude/projects/{sanitized-cwd}/{sessionId}.jsonl"
|
||||
- "Uses fs.unlink with graceful error handling for missing files"
|
||||
- "Checks two transcript path formats for backward compatibility"
|
||||
|
||||
Guidelines:
|
||||
- Start with filename or component when relevant
|
||||
- Be specific: include paths, function names, actual values
|
||||
- Each fact stands alone (no pronouns like "it" or "this")
|
||||
- 50-150 characters target
|
||||
- Focus on searchable technical details
|
||||
|
||||
## 4. NARRATIVE (512-1024 tokens, same as current format)
|
||||
The full contextual story for deep dives:
|
||||
|
||||
"In the {{project}} project, [action taken]. [Technical details: files, functions, concepts]. [Why this matters]."
|
||||
|
||||
This is the detailed explanation for when someone needs full context.
|
||||
|
||||
# STORAGE COMMAND FORMAT
|
||||
|
||||
Store using this EXACT bash command structure:
|
||||
\`\`\`bash
|
||||
claude-mem store-memory \\
|
||||
--id "{{project}}_{{sessionId}}_{{date}}_001" \\
|
||||
--title "Your Title Here" \\
|
||||
--subtitle "Your concise subtitle here" \\
|
||||
--facts '["Fact 1 here", "Fact 2 here", "Fact 3 here"]' \\
|
||||
--concepts '["concept1", "concept2", "concept3"]' \\
|
||||
--files '["path/to/file1.js", "path/to/file2.ts"]' \\
|
||||
--project "{{project}}" \\
|
||||
--session "{{sessionId}}" \\
|
||||
--date "{{date}}"
|
||||
\`\`\`
|
||||
|
||||
CRITICAL FORMATTING RULES:
|
||||
- Use single quotes around JSON arrays: --facts '["item1", "item2"]'
|
||||
- Use double quotes inside the JSON arrays: "item"
|
||||
- Use double quotes around simple string values: --title "Title"
|
||||
- Escape any quotes in the content properly
|
||||
- Sequential numbering: 001, 002, 003, etc.
|
||||
|
||||
Concepts: 2-5 broad categories (e.g., "hooks", "storage", "async-processing")
|
||||
Files: Actual file paths touched (e.g., "hooks/stop-streaming.js")
|
||||
|
||||
# EXAMPLE MEMORY
|
||||
|
||||
Tool response shows: [Read file hooks/stop-streaming.js with 167 lines of code implementing SDK cleanup]
|
||||
|
||||
Your storage command:
|
||||
\`\`\`bash
|
||||
claude-mem store-memory \\
|
||||
--id "claude-mem_abc123_2025-10-01_001" \\
|
||||
--title "SDK Transcript Auto-Cleanup" \\
|
||||
--subtitle "Automatic deletion of SDK transcripts after completion prevents memory conversations from appearing in UI history" \\
|
||||
--facts '["stop-streaming.js: Deletes SDK transcript after overview generation", "Path: ~/.claude/projects/{sanitized-cwd}/{sessionId}.jsonl", "Uses fs.unlink with error handling for missing files", "Prevents memory conversations from polluting Claude Code UI"]' \\
|
||||
--concepts '["cleanup", "SDK-lifecycle", "UX", "file-management"]' \\
|
||||
--files '["hooks/stop-streaming.js"]' \\
|
||||
--project "claude-mem" \\
|
||||
--session "abc123" \\
|
||||
--date "2025-10-01"
|
||||
\`\`\`
|
||||
|
||||
# STATE TRACKING
|
||||
|
||||
CRITICAL: Keep track of your memory counter across all tool messages.
|
||||
- Start at 001
|
||||
- Increment for each stored memory
|
||||
- Never repeat numbers
|
||||
- Each session has separate numbering
|
||||
|
||||
# SESSION END
|
||||
|
||||
At the end (when I send "SESSION ENDING"), generate an overview using:
|
||||
\`\`\`bash
|
||||
claude-mem store-overview --project "{{project}}" --session "{{sessionId}}" --content "2-3 sentence overview"
|
||||
\`\`\`
|
||||
|
||||
# IMPORTANT REMINDERS
|
||||
|
||||
- You're processing a DIFFERENT Claude Code session (not your own)
|
||||
- Use Bash tool to call claude-mem commands
|
||||
- Keep subtitles clear and informative (max 24 words)
|
||||
- Each fact is ONE specific thing (not multiple ideas)
|
||||
- Be selective - quality over quantity
|
||||
- Always increment memory numbers
|
||||
- Facts should be searchable (specific file names, paths, functions)
|
||||
|
||||
Ready for tool responses.`;
|
||||
var TOOL_MESSAGE = `# Tool Response {{timeFormatted}}
|
||||
|
||||
Tool: {{toolName}}
|
||||
User Context: "{{userPrompt}}"
|
||||
|
||||
\`\`\`
|
||||
{{toolResponse}}
|
||||
\`\`\`
|
||||
|
||||
Analyze and store if meaningful.`;
|
||||
var END_MESSAGE = `# SESSION ENDING
|
||||
|
||||
Review our entire conversation. Generate a concise 2-3 sentence overview of what was accomplished.
|
||||
|
||||
Store it using Bash:
|
||||
\`\`\`bash
|
||||
claude-mem store-overview --project "{{project}}" --session "{{sessionId}}" --content "YOUR_OVERVIEW_HERE"
|
||||
\`\`\`
|
||||
|
||||
Focus on: what was done, current state, key decisions, outcomes.`;
|
||||
var PROMPTS = {
|
||||
system: SYSTEM_PROMPT,
|
||||
tool: TOOL_MESSAGE,
|
||||
end: END_MESSAGE
|
||||
};
|
||||
|
||||
// src/prompts/hook-prompt-renderer.ts
|
||||
function substituteVariables(template, variables) {
|
||||
let result = template;
|
||||
for (const [key, value] of Object.entries(variables)) {
|
||||
const placeholder = `{{${key}}}`;
|
||||
result = result.split(placeholder).join(value);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
function truncate(text, maxLength) {
|
||||
if (text.length <= maxLength)
|
||||
return text;
|
||||
return text.slice(0, maxLength) + (text.length > maxLength ? "..." : "");
|
||||
}
|
||||
function formatTime(timestamp) {
|
||||
const timePart = timestamp.split("T")[1];
|
||||
if (!timePart)
|
||||
return "";
|
||||
return timePart.slice(0, 8);
|
||||
}
|
||||
function renderSystemPrompt(variables) {
|
||||
const userPromptTruncated = truncate(variables.userPrompt, HOOK_CONFIG.maxUserPromptLength);
|
||||
return substituteVariables(PROMPTS.system, {
|
||||
project: variables.project,
|
||||
sessionId: variables.sessionId,
|
||||
date: variables.date,
|
||||
userPrompt: userPromptTruncated
|
||||
});
|
||||
}
|
||||
function renderToolMessage(variables) {
|
||||
const userPromptTruncated = truncate(variables.userPrompt, HOOK_CONFIG.maxUserPromptLength);
|
||||
const toolResponseTruncated = truncate(variables.toolResponse, HOOK_CONFIG.maxToolResponseLength);
|
||||
const timeFormatted = formatTime(variables.timestamp);
|
||||
return substituteVariables(PROMPTS.tool, {
|
||||
toolName: variables.toolName,
|
||||
toolResponse: toolResponseTruncated,
|
||||
userPrompt: userPromptTruncated,
|
||||
timestamp: variables.timestamp,
|
||||
timeFormatted
|
||||
});
|
||||
}
|
||||
function renderEndMessage(variables) {
|
||||
return substituteVariables(PROMPTS.end, {
|
||||
project: variables.project,
|
||||
sessionId: variables.sessionId
|
||||
});
|
||||
}
|
||||
function renderPrompt(type, variables) {
|
||||
switch (type) {
|
||||
case "system":
|
||||
return renderSystemPrompt(variables);
|
||||
case "tool":
|
||||
return renderToolMessage(variables);
|
||||
case "end":
|
||||
return renderEndMessage(variables);
|
||||
default:
|
||||
throw new Error(`Unknown prompt type: ${type}`);
|
||||
}
|
||||
}
|
||||
export {
|
||||
renderToolMessage,
|
||||
renderSystemPrompt,
|
||||
renderPrompt,
|
||||
renderEndMessage,
|
||||
PROMPTS,
|
||||
HOOK_CONFIG
|
||||
};
|
||||
@@ -0,0 +1,217 @@
|
||||
// src/prompts/hook-prompts.config.ts
|
||||
var HOOK_CONFIG = {
|
||||
maxUserPromptLength: 200,
|
||||
maxToolResponseLength: 20000,
|
||||
sdk: {
|
||||
model: "claude-sonnet-4-5",
|
||||
allowedTools: ["Bash"],
|
||||
maxTokensSystem: 8192,
|
||||
maxTokensTool: 8192,
|
||||
maxTokensEnd: 2048
|
||||
}
|
||||
};
|
||||
var SYSTEM_PROMPT = `You are a semantic memory compressor for claude-mem. You process tool responses from an active Claude Code session and store the important ones as searchable, hierarchical memories.
|
||||
|
||||
# SESSION CONTEXT
|
||||
- Project: {{project}}
|
||||
- Session: {{sessionId}}
|
||||
- Date: {{date}}
|
||||
- User Request: "{{userPrompt}}"
|
||||
|
||||
# YOUR JOB
|
||||
|
||||
## FIRST: Generate Session Title
|
||||
|
||||
IMMEDIATELY generate a title and subtitle for this session based on the user request.
|
||||
|
||||
Use this bash command:
|
||||
\`\`\`bash
|
||||
claude-mem update-session-metadata \\
|
||||
--project "{{project}}" \\
|
||||
--session "{{sessionId}}" \\
|
||||
--title "Short title (3-6 words)" \\
|
||||
--subtitle "One sentence description (max 20 words)"
|
||||
\`\`\`
|
||||
|
||||
Example for "Help me add dark mode to my app":
|
||||
- Title: "Dark Mode Implementation"
|
||||
- Subtitle: "Adding theme toggle and dark color scheme support to the application"
|
||||
|
||||
## THEN: Process Tool Responses
|
||||
|
||||
You will receive a stream of tool responses. For each one:
|
||||
|
||||
1. ANALYZE: Does this contain information worth remembering?
|
||||
2. DECIDE: Should I store this or skip it?
|
||||
3. EXTRACT: What are the key semantic concepts?
|
||||
4. DECOMPOSE: Break into title + subtitle + atomic facts + narrative
|
||||
5. STORE: Use bash to save the hierarchical memory
|
||||
6. TRACK: Keep count of stored memories (001, 002, 003...)
|
||||
|
||||
# WHAT TO STORE
|
||||
|
||||
Store these:
|
||||
- File contents with logic, algorithms, or patterns
|
||||
- Search results revealing project structure
|
||||
- Build errors or test failures with context
|
||||
- Code revealing architecture or design decisions
|
||||
- Git diffs with significant changes
|
||||
- Command outputs showing system state
|
||||
|
||||
Skip these:
|
||||
- Simple status checks (git status with no changes)
|
||||
- Trivial edits (one-line config changes)
|
||||
- Repeated operations
|
||||
- Binary data or noise
|
||||
- Anything without semantic value
|
||||
|
||||
# HIERARCHICAL MEMORY FORMAT
|
||||
|
||||
Each memory has FOUR components:
|
||||
|
||||
## 1. TITLE (3-8 words)
|
||||
A scannable headline that captures the core action or topic.
|
||||
Examples:
|
||||
- "SDK Transcript Cleanup Implementation"
|
||||
- "Hook System Architecture Analysis"
|
||||
- "ChromaDB Migration Planning"
|
||||
|
||||
## 2. SUBTITLE (max 24 words)
|
||||
A concise, memorable summary that captures the essence of the change.
|
||||
Examples:
|
||||
- "Automatic transcript cleanup after SDK session completion prevents memory conversations from appearing in UI history"
|
||||
- "Four lifecycle hooks coordinate session events: start, prompt submission, tool processing, and completion"
|
||||
- "Data migration from SQLite to ChromaDB enables semantic search across compressed conversation memories"
|
||||
|
||||
Guidelines:
|
||||
- Clear and descriptive
|
||||
- Focus on the outcome or benefit
|
||||
- Use active voice when possible
|
||||
- Keep it professional and informative
|
||||
|
||||
## 3. ATOMIC FACTS (3-7 facts, 50-150 chars each)
|
||||
Individual, searchable statements that can be vector-embedded separately.
|
||||
Each fact is ONE specific piece of information.
|
||||
|
||||
Examples:
|
||||
- "stop-streaming.js: Auto-deletes SDK transcripts after completion"
|
||||
- "Path format: ~/.claude/projects/{sanitized-cwd}/{sessionId}.jsonl"
|
||||
- "Uses fs.unlink with graceful error handling for missing files"
|
||||
- "Checks two transcript path formats for backward compatibility"
|
||||
|
||||
Guidelines:
|
||||
- Start with filename or component when relevant
|
||||
- Be specific: include paths, function names, actual values
|
||||
- Each fact stands alone (no pronouns like "it" or "this")
|
||||
- 50-150 characters target
|
||||
- Focus on searchable technical details
|
||||
|
||||
## 4. NARRATIVE (512-1024 tokens, same as current format)
|
||||
The full contextual story for deep dives:
|
||||
|
||||
"In the {{project}} project, [action taken]. [Technical details: files, functions, concepts]. [Why this matters]."
|
||||
|
||||
This is the detailed explanation for when someone needs full context.
|
||||
|
||||
# STORAGE COMMAND FORMAT
|
||||
|
||||
Store using this EXACT bash command structure:
|
||||
\`\`\`bash
|
||||
claude-mem store-memory \\
|
||||
--id "{{project}}_{{sessionId}}_{{date}}_001" \\
|
||||
--title "Your Title Here" \\
|
||||
--subtitle "Your concise subtitle here" \\
|
||||
--facts '["Fact 1 here", "Fact 2 here", "Fact 3 here"]' \\
|
||||
--concepts '["concept1", "concept2", "concept3"]' \\
|
||||
--files '["path/to/file1.js", "path/to/file2.ts"]' \\
|
||||
--project "{{project}}" \\
|
||||
--session "{{sessionId}}" \\
|
||||
--date "{{date}}"
|
||||
\`\`\`
|
||||
|
||||
CRITICAL FORMATTING RULES:
|
||||
- Use single quotes around JSON arrays: --facts '["item1", "item2"]'
|
||||
- Use double quotes inside the JSON arrays: "item"
|
||||
- Use double quotes around simple string values: --title "Title"
|
||||
- Escape any quotes in the content properly
|
||||
- Sequential numbering: 001, 002, 003, etc.
|
||||
|
||||
Concepts: 2-5 broad categories (e.g., "hooks", "storage", "async-processing")
|
||||
Files: Actual file paths touched (e.g., "hooks/stop-streaming.js")
|
||||
|
||||
# EXAMPLE MEMORY
|
||||
|
||||
Tool response shows: [Read file hooks/stop-streaming.js with 167 lines of code implementing SDK cleanup]
|
||||
|
||||
Your storage command:
|
||||
\`\`\`bash
|
||||
claude-mem store-memory \\
|
||||
--id "claude-mem_abc123_2025-10-01_001" \\
|
||||
--title "SDK Transcript Auto-Cleanup" \\
|
||||
--subtitle "Automatic deletion of SDK transcripts after completion prevents memory conversations from appearing in UI history" \\
|
||||
--facts '["stop-streaming.js: Deletes SDK transcript after overview generation", "Path: ~/.claude/projects/{sanitized-cwd}/{sessionId}.jsonl", "Uses fs.unlink with error handling for missing files", "Prevents memory conversations from polluting Claude Code UI"]' \\
|
||||
--concepts '["cleanup", "SDK-lifecycle", "UX", "file-management"]' \\
|
||||
--files '["hooks/stop-streaming.js"]' \\
|
||||
--project "claude-mem" \\
|
||||
--session "abc123" \\
|
||||
--date "2025-10-01"
|
||||
\`\`\`
|
||||
|
||||
# STATE TRACKING
|
||||
|
||||
CRITICAL: Keep track of your memory counter across all tool messages.
|
||||
- Start at 001
|
||||
- Increment for each stored memory
|
||||
- Never repeat numbers
|
||||
- Each session has separate numbering
|
||||
|
||||
# SESSION END
|
||||
|
||||
At the end (when I send "SESSION ENDING"), generate an overview using:
|
||||
\`\`\`bash
|
||||
claude-mem store-overview --project "{{project}}" --session "{{sessionId}}" --content "2-3 sentence overview"
|
||||
\`\`\`
|
||||
|
||||
# IMPORTANT REMINDERS
|
||||
|
||||
- You're processing a DIFFERENT Claude Code session (not your own)
|
||||
- Use Bash tool to call claude-mem commands
|
||||
- Keep subtitles clear and informative (max 24 words)
|
||||
- Each fact is ONE specific thing (not multiple ideas)
|
||||
- Be selective - quality over quantity
|
||||
- Always increment memory numbers
|
||||
- Facts should be searchable (specific file names, paths, functions)
|
||||
|
||||
Ready for tool responses.`;
|
||||
var TOOL_MESSAGE = `# Tool Response {{timeFormatted}}
|
||||
|
||||
Tool: {{toolName}}
|
||||
User Context: "{{userPrompt}}"
|
||||
|
||||
\`\`\`
|
||||
{{toolResponse}}
|
||||
\`\`\`
|
||||
|
||||
Analyze and store if meaningful.`;
|
||||
var END_MESSAGE = `# SESSION ENDING
|
||||
|
||||
Review our entire conversation. Generate a concise 2-3 sentence overview of what was accomplished.
|
||||
|
||||
Store it using Bash:
|
||||
\`\`\`bash
|
||||
claude-mem store-overview --project "{{project}}" --session "{{sessionId}}" --content "YOUR_OVERVIEW_HERE"
|
||||
\`\`\`
|
||||
|
||||
Focus on: what was done, current state, key decisions, outcomes.`;
|
||||
var PROMPTS = {
|
||||
system: SYSTEM_PROMPT,
|
||||
tool: TOOL_MESSAGE,
|
||||
end: END_MESSAGE
|
||||
};
|
||||
export {
|
||||
TOOL_MESSAGE,
|
||||
SYSTEM_PROMPT,
|
||||
PROMPTS,
|
||||
HOOK_CONFIG,
|
||||
END_MESSAGE
|
||||
};
|
||||
@@ -0,0 +1,108 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Path resolver utility for Claude Memory hooks
|
||||
* Provides proper path handling using environment variables
|
||||
*/
|
||||
|
||||
import { join, basename } from 'path';
|
||||
import { homedir } from 'os';
|
||||
|
||||
/**
|
||||
* Gets the base data directory for claude-mem
|
||||
* @returns {string} Data directory path
|
||||
*/
|
||||
export function getDataDir() {
|
||||
return process.env.CLAUDE_MEM_DATA_DIR || join(homedir(), '.claude-mem');
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the settings file path
|
||||
* @returns {string} Settings file path
|
||||
*/
|
||||
export function getSettingsPath() {
|
||||
return join(getDataDir(), 'settings.json');
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the archives directory path
|
||||
* @returns {string} Archives directory path
|
||||
*/
|
||||
export function getArchivesDir() {
|
||||
return process.env.CLAUDE_MEM_ARCHIVES_DIR || join(getDataDir(), 'archives');
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the logs directory path
|
||||
* @returns {string} Logs directory path
|
||||
*/
|
||||
export function getLogsDir() {
|
||||
return process.env.CLAUDE_MEM_LOGS_DIR || join(getDataDir(), 'logs');
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the compact flag file path
|
||||
* @returns {string} Compact flag file path
|
||||
*/
|
||||
export function getCompactFlagPath() {
|
||||
return join(getDataDir(), '.compact-running');
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the claude-mem package root directory
|
||||
* @returns {Promise<string>} Package root path
|
||||
*/
|
||||
export async function getPackageRoot() {
|
||||
// Method 1: Check if we're running from development
|
||||
const devPath = join(homedir(), 'Scripts', 'claude-mem-source');
|
||||
const { existsSync } = await import('fs');
|
||||
if (existsSync(join(devPath, 'package.json'))) {
|
||||
return devPath;
|
||||
}
|
||||
|
||||
// Method 2: Follow the binary symlink
|
||||
try {
|
||||
const { execSync } = await import('child_process');
|
||||
const { realpathSync } = await import('fs');
|
||||
const binPath = execSync('which claude-mem', { encoding: 'utf8' }).trim();
|
||||
const realBinPath = realpathSync(binPath);
|
||||
// Binary is typically at package_root/dist/claude-mem.min.js
|
||||
return join(realBinPath, '../..');
|
||||
} catch {}
|
||||
|
||||
throw new Error('Cannot locate claude-mem package root');
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the project root directory
|
||||
* Uses CLAUDE_PROJECT_DIR environment variable if available, otherwise falls back to cwd
|
||||
* @returns {string} Project root path
|
||||
*/
|
||||
export function getProjectRoot() {
|
||||
return process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
||||
}
|
||||
|
||||
/**
|
||||
* Derives project name from CLAUDE_PROJECT_DIR or current working directory
|
||||
* Priority: CLAUDE_PROJECT_DIR > cwd parameter > process.cwd()
|
||||
* @param {string} [cwd] - Optional current working directory from hook payload
|
||||
* @returns {string} Project name (basename of project directory)
|
||||
*/
|
||||
export function getProjectName(cwd) {
|
||||
const projectRoot = process.env.CLAUDE_PROJECT_DIR || cwd || process.cwd();
|
||||
return basename(projectRoot);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all common paths used by hooks
|
||||
* @returns {Object} Object containing all common paths
|
||||
*/
|
||||
export function getPaths() {
|
||||
return {
|
||||
dataDir: getDataDir(),
|
||||
settingsPath: getSettingsPath(),
|
||||
archivesDir: getArchivesDir(),
|
||||
logsDir: getLogsDir(),
|
||||
compactFlagPath: getCompactFlagPath()
|
||||
};
|
||||
}
|
||||
Executable
+134
@@ -0,0 +1,134 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Stop Hook - Simple Orchestrator
|
||||
*
|
||||
* Signals session end to SDK, which generates and stores the overview via CLI.
|
||||
* Cleans up SDK transcript from UI.
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import { query } from '@anthropic-ai/claude-agent-sdk';
|
||||
import { renderEndMessage, HOOK_CONFIG } from './shared/hook-prompt-renderer.js';
|
||||
import { getProjectName } from './shared/path-resolver.js';
|
||||
import { initializeDatabase, getActiveStreamingSessionsForProject, markStreamingSessionCompleted } from './shared/hook-helpers.js';
|
||||
|
||||
const HOOKS_LOG = path.join(process.env.HOME || '', '.claude-mem', 'logs', 'hooks.log');
|
||||
|
||||
function debugLog(message, data = {}) {
|
||||
if (process.env.CLAUDE_MEM_DEBUG === 'true') {
|
||||
const timestamp = new Date().toISOString();
|
||||
const logLine = `[${timestamp}] HOOK DEBUG: ${message} ${JSON.stringify(data)}\n`;
|
||||
try {
|
||||
fs.appendFileSync(HOOKS_LOG, logLine);
|
||||
process.stderr.write(logLine);
|
||||
} catch (error) {
|
||||
// Silent fail on log errors
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// MAIN
|
||||
// =============================================================================
|
||||
|
||||
let input = '';
|
||||
process.stdin.setEncoding('utf8');
|
||||
process.stdin.on('data', (chunk) => { input += chunk; });
|
||||
|
||||
process.stdin.on('end', async () => {
|
||||
let payload;
|
||||
try {
|
||||
payload = input ? JSON.parse(input) : {};
|
||||
} catch (error) {
|
||||
debugLog('Stop: JSON parse error', { error: error.message });
|
||||
console.log(JSON.stringify({ continue: true, suppressOutput: true }));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const { cwd } = payload;
|
||||
const project = cwd ? getProjectName(cwd) : 'unknown';
|
||||
|
||||
// Immediately clear activity flag for UI indicator
|
||||
const activityFlagPath = path.join(process.env.HOME || '', '.claude-mem', 'activity.flag');
|
||||
try {
|
||||
fs.writeFileSync(activityFlagPath, JSON.stringify({ active: false, timestamp: Date.now() }));
|
||||
} catch (error) {
|
||||
// Silent fail - non-critical
|
||||
}
|
||||
|
||||
// Return immediately with async mode
|
||||
console.log(JSON.stringify({ async: true, asyncTimeout: 180000 }));
|
||||
|
||||
try {
|
||||
// Load SDK session info from database
|
||||
const db = initializeDatabase();
|
||||
|
||||
const sessions = getActiveStreamingSessionsForProject(db, project);
|
||||
if (!sessions || sessions.length === 0) {
|
||||
debugLog('Stop: No streaming session found', { project });
|
||||
db.close();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const sessionData = sessions[0];
|
||||
const sdkSessionId = sessionData.sdk_session_id;
|
||||
const claudeSessionId = sessionData.claude_session_id;
|
||||
|
||||
debugLog('Stop: Ending SDK session', { sdkSessionId, claudeSessionId });
|
||||
|
||||
// Build end message - SDK will call `claude-mem store-overview` and `chroma_add_documents`
|
||||
const message = renderEndMessage({
|
||||
project,
|
||||
sessionId: claudeSessionId
|
||||
});
|
||||
|
||||
// Send end message and wait for SDK to complete
|
||||
const response = query({
|
||||
prompt: message,
|
||||
options: {
|
||||
model: HOOK_CONFIG.sdk.model,
|
||||
resume: sdkSessionId,
|
||||
allowedTools: HOOK_CONFIG.sdk.allowedTools,
|
||||
maxTokens: HOOK_CONFIG.sdk.maxTokensEnd,
|
||||
cwd // Must match where transcript was created
|
||||
}
|
||||
});
|
||||
|
||||
// Consume the response stream (wait for SDK to finish storing via CLI)
|
||||
for await (const msg of response) {
|
||||
if (msg.type === 'assistant' && msg.message?.content) {
|
||||
for (const block of msg.message.content) {
|
||||
if (block.type === 'tool_use') {
|
||||
debugLog('Stop: SDK tool call', { tool: block.name });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
debugLog('Stop: SDK session ended', { sdkSessionId });
|
||||
|
||||
// Delete SDK memories transcript from Claude Code UI
|
||||
const sanitizedCwd = cwd.replace(/\//g, '-');
|
||||
const projectsDir = path.join(process.env.HOME, '.claude', 'projects', sanitizedCwd);
|
||||
const memoriesTranscriptPath = path.join(projectsDir, `${sdkSessionId}.jsonl`);
|
||||
|
||||
if (fs.existsSync(memoriesTranscriptPath)) {
|
||||
fs.unlinkSync(memoriesTranscriptPath);
|
||||
debugLog('Stop: Cleaned up memories transcript', { memoriesTranscriptPath });
|
||||
}
|
||||
|
||||
// Mark session as completed in database
|
||||
markStreamingSessionCompleted(db, sessionData.id);
|
||||
debugLog('Stop: Session ended and marked complete', { project, sessionId: sessionData.id });
|
||||
|
||||
// Close database connection
|
||||
db.close();
|
||||
} catch (error) {
|
||||
debugLog('Stop: Error ending session', { error: error.message });
|
||||
}
|
||||
|
||||
// Exit cleanly after async processing completes
|
||||
process.exit(0);
|
||||
});
|
||||
Executable
+157
@@ -0,0 +1,157 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* User Prompt Submit Hook - Streaming SDK Version
|
||||
*
|
||||
* Starts a streaming SDK session that will process tool responses in real-time.
|
||||
* Saves the SDK session ID for post-tool-use and stop hooks to resume.
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { query } from '@anthropic-ai/claude-agent-sdk';
|
||||
import { renderSystemPrompt, HOOK_CONFIG } from './shared/hook-prompt-renderer.js';
|
||||
import { getProjectName } from './shared/path-resolver.js';
|
||||
import { initializeDatabase, createStreamingSession, updateStreamingSession } from './shared/hook-helpers.js';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const HOOKS_LOG = path.join(process.env.HOME || '', '.claude-mem', 'logs', 'hooks.log');
|
||||
|
||||
function debugLog(message, data = {}) {
|
||||
if (process.env.CLAUDE_MEM_DEBUG === 'true') {
|
||||
const timestamp = new Date().toISOString();
|
||||
const logLine = `[${timestamp}] HOOK DEBUG: ${message} ${JSON.stringify(data)}\n`;
|
||||
try {
|
||||
fs.appendFileSync(HOOKS_LOG, logLine);
|
||||
process.stderr.write(logLine);
|
||||
} catch (error) {
|
||||
// Silent fail on log errors
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Removed: buildStreamingSystemPrompt function
|
||||
// Now using centralized config from hook-prompt-renderer.js
|
||||
|
||||
// =============================================================================
|
||||
// MAIN
|
||||
// =============================================================================
|
||||
|
||||
let input = '';
|
||||
process.stdin.setEncoding('utf8');
|
||||
process.stdin.on('data', (chunk) => { input += chunk; });
|
||||
|
||||
process.stdin.on('end', async () => {
|
||||
let payload;
|
||||
try {
|
||||
payload = input ? JSON.parse(input) : {};
|
||||
} catch (error) {
|
||||
debugLog('UserPromptSubmit: JSON parse error', { error: error.message });
|
||||
console.log(JSON.stringify({ continue: true, suppressOutput: true }));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const { prompt, cwd, session_id, timestamp } = payload;
|
||||
const project = cwd ? getProjectName(cwd) : 'unknown';
|
||||
const date = timestamp ? timestamp.split('T')[0] : new Date().toISOString().split('T')[0];
|
||||
|
||||
debugLog('UserPromptSubmit: Starting streaming session', { project, session_id });
|
||||
|
||||
// Immediately signal activity start for UI indicator
|
||||
const activityFlagPath = path.join(process.env.HOME || '', '.claude-mem', 'activity.flag');
|
||||
try {
|
||||
fs.writeFileSync(activityFlagPath, JSON.stringify({ active: true, project, timestamp: Date.now() }));
|
||||
} catch (error) {
|
||||
// Silent fail - non-critical
|
||||
}
|
||||
|
||||
// Generate title and subtitle non-blocking
|
||||
if (prompt && session_id && project) {
|
||||
import('child_process').then(({ spawn }) => {
|
||||
const titleProcess = spawn('claude-mem', [
|
||||
'generate-title',
|
||||
'--save',
|
||||
'--project', project,
|
||||
'--session', session_id,
|
||||
prompt
|
||||
], {
|
||||
stdio: 'ignore',
|
||||
detached: true
|
||||
});
|
||||
titleProcess.unref();
|
||||
}).catch(error => {
|
||||
debugLog('UserPromptSubmit: Error spawning title generator', { error: error.message });
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
// Initialize database and create session record FIRST
|
||||
const db = initializeDatabase();
|
||||
|
||||
// Create session record immediately - this gives us a tracking ID
|
||||
const sessionRecord = createStreamingSession(db, {
|
||||
claude_session_id: session_id,
|
||||
project,
|
||||
user_prompt: prompt,
|
||||
started_at: timestamp
|
||||
});
|
||||
|
||||
debugLog('UserPromptSubmit: Created session record', {
|
||||
internalId: sessionRecord.id,
|
||||
claudeSessionId: session_id
|
||||
});
|
||||
|
||||
// Build system prompt using centralized config
|
||||
const systemPrompt = renderSystemPrompt({
|
||||
project,
|
||||
sessionId: session_id,
|
||||
date,
|
||||
userPrompt: prompt || ''
|
||||
});
|
||||
|
||||
// Start SDK session using centralized config
|
||||
const response = query({
|
||||
prompt: systemPrompt,
|
||||
options: {
|
||||
model: HOOK_CONFIG.sdk.model,
|
||||
allowedTools: HOOK_CONFIG.sdk.allowedTools,
|
||||
maxTokens: HOOK_CONFIG.sdk.maxTokensSystem,
|
||||
cwd // SDK will save transcript in this directory
|
||||
}
|
||||
});
|
||||
|
||||
// Wait for session ID from init message and consume entire stream
|
||||
let sdkSessionId = null;
|
||||
for await (const message of response) {
|
||||
if (message.type === 'system' && message.subtype === 'init') {
|
||||
sdkSessionId = message.session_id;
|
||||
debugLog('UserPromptSubmit: Got SDK session ID', { sdkSessionId });
|
||||
}
|
||||
// Don't break - consume entire stream so transcript gets written
|
||||
}
|
||||
|
||||
if (sdkSessionId) {
|
||||
// Update session record with SDK session ID
|
||||
updateStreamingSession(db, sessionRecord.id, {
|
||||
sdk_session_id: sdkSessionId
|
||||
});
|
||||
|
||||
debugLog('UserPromptSubmit: SDK session started', {
|
||||
internalId: sessionRecord.id,
|
||||
sdkSessionId
|
||||
});
|
||||
}
|
||||
|
||||
// Close database connection
|
||||
db.close();
|
||||
} catch (error) {
|
||||
debugLog('UserPromptSubmit: Error starting SDK session', { error: error.message });
|
||||
}
|
||||
|
||||
// Return success to Claude Code
|
||||
console.log(JSON.stringify({ continue: true, suppressOutput: true }));
|
||||
process.exit(0);
|
||||
});
|
||||
@@ -1,81 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Pre-Compact Hook for Claude Memory System
|
||||
*
|
||||
* Updated to use the centralized PromptOrchestrator and HookTemplates system.
|
||||
* This hook validates the pre-compact request and executes compression using
|
||||
* standardized response templates for consistent Claude Code integration.
|
||||
*/
|
||||
|
||||
import { loadCliCommand } from './shared/config-loader.js';
|
||||
import {
|
||||
createHookResponse,
|
||||
executeCliCommand,
|
||||
validateHookPayload,
|
||||
debugLog
|
||||
} from './shared/hook-helpers.js';
|
||||
|
||||
const cliCommand = loadCliCommand();
|
||||
|
||||
// Read input from stdin
|
||||
let input = '';
|
||||
process.stdin.on('data', chunk => {
|
||||
input += chunk;
|
||||
});
|
||||
|
||||
process.stdin.on('end', async () => {
|
||||
try {
|
||||
const payload = JSON.parse(input);
|
||||
debugLog('Pre-compact hook started', { payload });
|
||||
|
||||
// Validate payload using centralized validation
|
||||
const validation = validateHookPayload(payload, 'PreCompact');
|
||||
if (!validation.valid) {
|
||||
const response = createHookResponse('PreCompact', false, { reason: validation.error });
|
||||
console.log(JSON.stringify(response));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Check for environment-based blocking conditions
|
||||
if (payload.trigger === 'auto' && process.env.DISABLE_AUTO_COMPRESSION === 'true') {
|
||||
debugLog('Auto-compression disabled by configuration');
|
||||
const response = createHookResponse('PreCompact', false, {
|
||||
reason: 'Auto-compression disabled by configuration'
|
||||
});
|
||||
console.log(JSON.stringify(response));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Execute compression using standardized CLI execution helper
|
||||
debugLog('Executing compression command', {
|
||||
command: cliCommand,
|
||||
args: ['compress', payload.transcript_path]
|
||||
});
|
||||
|
||||
const result = await executeCliCommand(cliCommand, ['compress', payload.transcript_path]);
|
||||
|
||||
if (!result.success) {
|
||||
debugLog('Compression command failed', { stderr: result.stderr });
|
||||
const response = createHookResponse('PreCompact', false, {
|
||||
reason: `Compression failed: ${result.stderr || 'Unknown error'}`
|
||||
});
|
||||
console.log(JSON.stringify(response));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Success - create standardized approval response using HookTemplates
|
||||
debugLog('Compression completed successfully');
|
||||
const response = createHookResponse('PreCompact', true);
|
||||
console.log(JSON.stringify(response));
|
||||
process.exit(0);
|
||||
|
||||
} catch (error) {
|
||||
debugLog('Pre-compact hook error', { error: error.message });
|
||||
const response = createHookResponse('PreCompact', false, {
|
||||
reason: `Hook execution error: ${error.message}`
|
||||
});
|
||||
console.log(JSON.stringify(response));
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
@@ -1,58 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Session End Hook - Handles session end events including /clear
|
||||
*/
|
||||
|
||||
import { loadCliCommand } from './shared/config-loader.js';
|
||||
import { execSync } from 'child_process';
|
||||
import { join } from 'path';
|
||||
import { homedir } from 'os';
|
||||
import { existsSync, readFileSync } from 'fs';
|
||||
|
||||
const cliCommand = loadCliCommand();
|
||||
|
||||
// Check if save-on-clear is enabled
|
||||
function isSaveOnClearEnabled() {
|
||||
const settingsPath = join(homedir(), '.claude-mem', 'settings.json');
|
||||
if (existsSync(settingsPath)) {
|
||||
try {
|
||||
const settings = JSON.parse(readFileSync(settingsPath, 'utf8'));
|
||||
return settings.saveMemoriesOnClear === true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Read input
|
||||
let input = '';
|
||||
process.stdin.on('data', chunk => {
|
||||
input += chunk;
|
||||
});
|
||||
|
||||
process.stdin.on('end', async () => {
|
||||
const data = JSON.parse(input);
|
||||
|
||||
// Check if this is a clear event and save-on-clear is enabled
|
||||
if (data.reason === 'clear' && isSaveOnClearEnabled()) {
|
||||
console.error('🧠 Saving memories before clearing context...');
|
||||
|
||||
try {
|
||||
// Use the CLI to compress current transcript
|
||||
execSync(`${cliCommand} compress --output ${homedir()}/.claude-mem/archives`, {
|
||||
stdio: 'inherit',
|
||||
env: { ...process.env, CLAUDE_MEM_SILENT: 'true' }
|
||||
});
|
||||
|
||||
console.error('✅ Memories saved successfully');
|
||||
} catch (error) {
|
||||
console.error('[session-end] Failed to save memories:', error.message);
|
||||
// Don't block the clear operation if memory saving fails
|
||||
}
|
||||
}
|
||||
|
||||
// Always continue
|
||||
console.log(JSON.stringify({ continue: true }));
|
||||
});
|
||||
@@ -1,166 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Session Start Hook - Load context when Claude Code starts
|
||||
*
|
||||
* Updated to use the centralized PromptOrchestrator and HookTemplates system.
|
||||
* This hook loads previous session context using standardized formatting and
|
||||
* provides rich context messages for Claude Code integration.
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
import { loadCliCommand } from './shared/config-loader.js';
|
||||
import {
|
||||
createHookResponse,
|
||||
formatSessionStartContext,
|
||||
executeCliCommand,
|
||||
parseContextData,
|
||||
validateHookPayload,
|
||||
debugLog
|
||||
} from './shared/hook-helpers.js';
|
||||
|
||||
const cliCommand = loadCliCommand();
|
||||
|
||||
// Read input from stdin
|
||||
let input = '';
|
||||
process.stdin.on('data', chunk => {
|
||||
input += chunk;
|
||||
});
|
||||
|
||||
process.stdin.on('end', async () => {
|
||||
try {
|
||||
const payload = JSON.parse(input);
|
||||
debugLog('Session start hook started', { payload });
|
||||
|
||||
// Validate payload using centralized validation
|
||||
const validation = validateHookPayload(payload, 'SessionStart');
|
||||
if (!validation.valid) {
|
||||
debugLog('Payload validation failed', { error: validation.error });
|
||||
// For session start, continue even with invalid payload but log the error
|
||||
const response = createHookResponse('SessionStart', false, {
|
||||
error: `Payload validation failed: ${validation.error}`
|
||||
});
|
||||
console.log(JSON.stringify(response));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Skip load-context when source is "resume" to avoid duplicate context
|
||||
if (payload.source === 'resume') {
|
||||
debugLog('Skipping load-context for resume source');
|
||||
// Output nothing at all for resume - no message, no context
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Extract project name from current working directory and sanitize
|
||||
const rawProjectName = path.basename(process.cwd());
|
||||
const projectName = rawProjectName.replace(/-/g, '_');
|
||||
debugLog('Extracted project name', { rawProjectName, projectName });
|
||||
|
||||
// Load context using standardized CLI execution helper
|
||||
const contextResult = await executeCliCommand(cliCommand, [
|
||||
'load-context',
|
||||
'--format', 'session-start',
|
||||
'--project', projectName
|
||||
]);
|
||||
|
||||
if (!contextResult.success) {
|
||||
debugLog('Context loading failed', { stderr: contextResult.stderr });
|
||||
// Don't fail the session start, just provide error context
|
||||
const response = createHookResponse('SessionStart', false, {
|
||||
error: contextResult.stderr || 'Failed to load context'
|
||||
});
|
||||
console.log(JSON.stringify(response));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const rawContext = contextResult.stdout;
|
||||
debugLog('Raw context loaded', { contextLength: rawContext.length });
|
||||
|
||||
// Check if the output is actually an error message (starts with ❌)
|
||||
if (rawContext && rawContext.trim().startsWith('❌')) {
|
||||
debugLog('Detected error message in stdout', { rawContext });
|
||||
// Extract the clean error message without the emoji and format
|
||||
const errorMatch = rawContext.match(/❌\s*[^:]+:\s*([^\n]+)(?:\n\n💡\s*(.+))?/);
|
||||
let errorMsg = 'No previous memories found';
|
||||
let suggestion = '';
|
||||
|
||||
if (errorMatch) {
|
||||
errorMsg = errorMatch[1] || errorMsg;
|
||||
suggestion = errorMatch[2] || '';
|
||||
}
|
||||
|
||||
// Create a clean response without duplicating the error formatting
|
||||
const response = createHookResponse('SessionStart', false, {
|
||||
error: errorMsg + (suggestion ? `. ${suggestion}` : '')
|
||||
});
|
||||
console.log(JSON.stringify(response));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (!rawContext || !rawContext.trim()) {
|
||||
debugLog('No context available, creating empty response');
|
||||
// No context available - use standardized empty response
|
||||
const response = createHookResponse('SessionStart', true);
|
||||
console.log(JSON.stringify(response));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Parse context data and format using centralized templates
|
||||
const contextData = parseContextData(rawContext);
|
||||
contextData.projectName = projectName;
|
||||
|
||||
// If we have raw context (not structured data), use it directly
|
||||
let formattedContext;
|
||||
if (contextData.rawContext) {
|
||||
formattedContext = contextData.rawContext;
|
||||
} else {
|
||||
// Use standardized formatting for structured context
|
||||
formattedContext = formatSessionStartContext(contextData);
|
||||
}
|
||||
|
||||
debugLog('Context formatted successfully', {
|
||||
memoryCount: contextData.memoryCount,
|
||||
hasStructuredData: !contextData.rawContext
|
||||
});
|
||||
|
||||
// Create standardized session start response using HookTemplates
|
||||
const response = createHookResponse('SessionStart', true, {
|
||||
context: formattedContext
|
||||
});
|
||||
|
||||
console.log(JSON.stringify(response));
|
||||
process.exit(0);
|
||||
|
||||
} catch (error) {
|
||||
debugLog('Session start hook error', { error: error.message });
|
||||
// Even on error, continue the session with error information
|
||||
const response = createHookResponse('SessionStart', false, {
|
||||
error: `Hook execution error: ${error.message}`
|
||||
});
|
||||
console.log(JSON.stringify(response));
|
||||
process.exit(0);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Extracts project name from transcript path
|
||||
* @param {string} transcriptPath - Path to transcript file
|
||||
* @returns {string|null} Extracted project name or null
|
||||
*/
|
||||
function extractProjectName(transcriptPath) {
|
||||
if (!transcriptPath) return null;
|
||||
|
||||
// Look for project pattern: /path/to/PROJECT_NAME/.claude/
|
||||
// Need to get PROJECT_NAME, not the parent directory
|
||||
const parts = transcriptPath.split('/');
|
||||
const claudeIndex = parts.indexOf('.claude');
|
||||
|
||||
if (claudeIndex > 0) {
|
||||
// Get the directory immediately before .claude
|
||||
return parts[claudeIndex - 1];
|
||||
}
|
||||
|
||||
// Fall back to directory containing the transcript
|
||||
const dir = path.dirname(transcriptPath);
|
||||
return path.basename(dir);
|
||||
}
|
||||
-58
@@ -1,58 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Claude Mem Installation Script
|
||||
|
||||
set -e
|
||||
|
||||
VERSION="${1:-latest}"
|
||||
PLATFORM=""
|
||||
ARCH=$(uname -m)
|
||||
OS=$(uname -s)
|
||||
|
||||
# Detect platform
|
||||
case "$OS" in
|
||||
Darwin)
|
||||
if [ "$ARCH" = "arm64" ]; then
|
||||
PLATFORM="macos-arm64"
|
||||
BINARY="claude-mem-macos-arm64"
|
||||
else
|
||||
PLATFORM="macos-x64"
|
||||
BINARY="claude-mem-macos-x64"
|
||||
fi
|
||||
;;
|
||||
Linux)
|
||||
if [ "$ARCH" = "aarch64" ]; then
|
||||
PLATFORM="linux-arm64"
|
||||
BINARY="claude-mem-linux-arm64"
|
||||
else
|
||||
PLATFORM="linux-x64"
|
||||
BINARY="claude-mem-linux"
|
||||
fi
|
||||
;;
|
||||
MINGW*|MSYS*|CYGWIN*)
|
||||
PLATFORM="windows-x64"
|
||||
BINARY="claude-mem.exe"
|
||||
;;
|
||||
*)
|
||||
echo "Unsupported platform: $OS $ARCH"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "📥 Downloading Claude Mem for $PLATFORM..."
|
||||
|
||||
# Download binary from GitHub releases
|
||||
if [ "$VERSION" = "latest" ]; then
|
||||
DOWNLOAD_URL="https://github.com/thedotmack/claude-mem/releases/latest/download/${BINARY}"
|
||||
else
|
||||
DOWNLOAD_URL="https://github.com/thedotmack/claude-mem/releases/download/${VERSION}/${BINARY}"
|
||||
fi
|
||||
|
||||
curl -L -o claude-mem "$DOWNLOAD_URL"
|
||||
|
||||
# Make executable (non-Windows)
|
||||
if [ "$OS" != "MINGW" ] && [ "$OS" != "MSYS" ] && [ "$OS" != "CYGWIN" ]; then
|
||||
chmod +x claude-mem
|
||||
fi
|
||||
|
||||
echo "✅ Claude Mem installed successfully!"
|
||||
echo "Run ./claude-mem --help to get started"
|
||||
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"name": "claude-mem",
|
||||
"version": "3.9.11",
|
||||
"description": "Memory compression system for Claude Code - persist context across sessions",
|
||||
"keywords": [
|
||||
"claude",
|
||||
"claude-agent-sdk",
|
||||
"mcp",
|
||||
"memory",
|
||||
"compression",
|
||||
"knowledge-graph",
|
||||
"transcript",
|
||||
"cli",
|
||||
"typescript",
|
||||
"bun"
|
||||
],
|
||||
"author": "Alex Newman",
|
||||
"license": "SEE LICENSE IN LICENSE",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/thedotmack/claude-mem.git"
|
||||
},
|
||||
"homepage": "https://github.com/thedotmack/claude-mem#readme",
|
||||
"bugs": {
|
||||
"url": "https://github.com/thedotmack/claude-mem/issues"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://registry.npmjs.org/"
|
||||
},
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"claude-mem": "./dist/claude-mem.min.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@anthropic-ai/claude-agent-sdk": "^0.1.0",
|
||||
"@clack/prompts": "^0.11.0",
|
||||
"boxen": "^8.0.1",
|
||||
"chalk": "^5.6.0",
|
||||
"commander": "^14.0.0",
|
||||
"glob": "^11.0.3",
|
||||
"gradient-string": "^3.0.0",
|
||||
"handlebars": "^4.7.8"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"hook-templates",
|
||||
"commands",
|
||||
"src",
|
||||
".mcp.json",
|
||||
"CHANGELOG.md"
|
||||
]
|
||||
}
|
||||
+275
@@ -0,0 +1,275 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// <Block> 1.1 ====================================
|
||||
// CLI Dependencies and Imports Setup
|
||||
// Natural pattern: Import what you need before using it
|
||||
import { Command } from 'commander';
|
||||
import { PACKAGE_NAME, PACKAGE_VERSION, PACKAGE_DESCRIPTION } from '../shared/config.js';
|
||||
|
||||
// Import command handlers
|
||||
import { install } from '../commands/install.js';
|
||||
import { uninstall } from '../commands/uninstall.js';
|
||||
import { status } from '../commands/status.js';
|
||||
import { logs } from '../commands/logs.js';
|
||||
import { loadContext } from '../commands/load-context.js';
|
||||
import { trash } from '../commands/trash.js';
|
||||
import { viewTrash } from '../commands/trash-view.js';
|
||||
import { emptyTrash } from '../commands/trash-empty.js';
|
||||
import { restore } from '../commands/restore.js';
|
||||
import { changelog } from '../commands/changelog.js';
|
||||
import { doctor } from '../commands/doctor.js';
|
||||
import { storeMemory } from '../commands/store-memory.js';
|
||||
import { storeOverview } from '../commands/store-overview.js';
|
||||
import { updateSessionMetadata } from '../commands/update-session-metadata.js';
|
||||
import { generateTitle } from '../commands/generate-title.js';
|
||||
import {
|
||||
executeChromaMCPTool,
|
||||
loadChromaMCPTools,
|
||||
generateCommandOptions
|
||||
} from '../commands/chroma-mcp.js';
|
||||
|
||||
const program = new Command();
|
||||
// </Block> =======================================
|
||||
|
||||
// <Block> 1.2 ====================================
|
||||
// Program Configuration
|
||||
// Natural pattern: Configure program metadata first
|
||||
program
|
||||
.name(PACKAGE_NAME)
|
||||
.description(PACKAGE_DESCRIPTION)
|
||||
.version(PACKAGE_VERSION);
|
||||
// </Block> =======================================
|
||||
|
||||
// <Block> 1.3 ====================================
|
||||
// Install Command Definition
|
||||
// Natural pattern: Define command with its options and handler
|
||||
// Install command
|
||||
program
|
||||
.command('install')
|
||||
.description('Install Claude Code hooks for automatic compression')
|
||||
.option('--user', 'Install for current user (default)')
|
||||
.option('--project', 'Install for current project only')
|
||||
.option('--local', 'Install to custom local directory')
|
||||
.option('--path <path>', 'Custom installation path (with --local)')
|
||||
.option('--timeout <ms>', 'Hook execution timeout in milliseconds', '180000')
|
||||
.option('--skip-mcp', 'Skip Chroma MCP server installation')
|
||||
.option('--force', 'Force installation even if already installed')
|
||||
.action(install);
|
||||
// </Block> =======================================
|
||||
|
||||
// <Block> 1.5 ====================================
|
||||
// Uninstall Command Definition
|
||||
// Natural pattern: Define command with its options and handler
|
||||
// Uninstall command
|
||||
program
|
||||
.command('uninstall')
|
||||
.description('Remove Claude Code hooks')
|
||||
.option('--user', 'Remove from user settings (default)')
|
||||
.option('--project', 'Remove from project settings')
|
||||
.option('--all', 'Remove from both user and project settings')
|
||||
.action(uninstall);
|
||||
// </Block> =======================================
|
||||
|
||||
|
||||
// <Block> 1.6 ====================================
|
||||
// Status Command Definition
|
||||
// Natural pattern: Define command with its handler
|
||||
// Status command
|
||||
program
|
||||
.command('status')
|
||||
.description('Check installation status of Claude Memory System')
|
||||
.action(status);
|
||||
|
||||
// Doctor command
|
||||
program
|
||||
.command('doctor')
|
||||
.description('Run environment and pipeline diagnostics for rolling memory')
|
||||
.option('--json', 'Output JSON instead of text')
|
||||
.action(async (options: any) => {
|
||||
try {
|
||||
await doctor(options);
|
||||
} catch (error: any) {
|
||||
console.error(`doctor failed: ${error.message || error}`);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
});
|
||||
// </Block> =======================================
|
||||
|
||||
// <Block> 1.7 ====================================
|
||||
// Logs Command Definition
|
||||
// Natural pattern: Define command with its options and handler
|
||||
// Logs command
|
||||
program
|
||||
.command('logs')
|
||||
.description('View claude-mem operation logs')
|
||||
.option('--debug', 'Show debug logs only')
|
||||
.option('--error', 'Show error logs only')
|
||||
.option('--tail [n]', 'Show last n lines', '50')
|
||||
.option('--follow', 'Follow log output')
|
||||
.action(logs);
|
||||
// </Block> =======================================
|
||||
|
||||
// <Block> 1.8 ====================================
|
||||
// Load-Context Command Definition
|
||||
// Natural pattern: Define command with its options and handler
|
||||
// Load-context command
|
||||
program
|
||||
.command('load-context')
|
||||
.description('Load compressed memories for current session')
|
||||
.option('--project <name>', 'Filter by project name')
|
||||
.option('--count <n>', 'Number of memories to load', '10')
|
||||
.option('--raw', 'Output raw JSON instead of formatted text')
|
||||
.option('--format <type>', 'Output format: json, session-start, or default')
|
||||
.action(loadContext);
|
||||
// </Block> =======================================
|
||||
|
||||
// <Block> 1.9 ====================================
|
||||
// Trash and Restore Commands Definition
|
||||
// Natural pattern: Define commands for safe file operations
|
||||
|
||||
// Trash command with subcommands
|
||||
const trashCmd = program
|
||||
.command('trash')
|
||||
.description('Manage trash bin for safe file deletion')
|
||||
.argument('[files...]', 'Files to move to trash')
|
||||
.option('-r, --recursive', 'Remove directories recursively')
|
||||
.option('-R', 'Remove directories recursively (same as -r)')
|
||||
.option('-f, --force', 'Suppress errors for nonexistent files')
|
||||
.action(async (files: string[] | undefined, options: any) => {
|
||||
// If no files provided, show help
|
||||
if (!files || files.length === 0) {
|
||||
trashCmd.outputHelp();
|
||||
return;
|
||||
}
|
||||
|
||||
// Map -R to recursive
|
||||
if (options.R) options.recursive = true;
|
||||
|
||||
await trash(files, {
|
||||
force: options.force,
|
||||
recursive: options.recursive
|
||||
});
|
||||
});
|
||||
|
||||
// Trash view subcommand
|
||||
trashCmd
|
||||
.command('view')
|
||||
.description('View contents of trash bin')
|
||||
.action(viewTrash);
|
||||
|
||||
// Trash empty subcommand
|
||||
trashCmd
|
||||
.command('empty')
|
||||
.description('Permanently delete all files in trash')
|
||||
.option('-f, --force', 'Skip confirmation prompt')
|
||||
.action(emptyTrash);
|
||||
|
||||
// Restore command
|
||||
program
|
||||
.command('restore')
|
||||
.description('Restore files from trash interactively')
|
||||
.action(restore);
|
||||
// </Block> =======================================
|
||||
|
||||
// Store memory command (for SDK streaming)
|
||||
program
|
||||
.command('store-memory')
|
||||
.description('Store a memory to all storage layers (used by SDK)')
|
||||
.requiredOption('--id <id>', 'Memory ID')
|
||||
.requiredOption('--project <project>', 'Project name')
|
||||
.requiredOption('--session <session>', 'Session ID')
|
||||
.requiredOption('--date <date>', 'Date (YYYY-MM-DD)')
|
||||
.requiredOption('--title <title>', 'Memory title (3-8 words)')
|
||||
.requiredOption('--subtitle <subtitle>', 'Memory subtitle (max 24 words)')
|
||||
.requiredOption('--facts <json>', 'Atomic facts as JSON array')
|
||||
.option('--concepts <json>', 'Concept tags as JSON array')
|
||||
.option('--files <json>', 'Files touched as JSON array')
|
||||
.action(storeMemory);
|
||||
|
||||
// Store overview command (for SDK streaming)
|
||||
program
|
||||
.command('store-overview')
|
||||
.description('Store a session overview (used by SDK)')
|
||||
.requiredOption('--project <project>', 'Project name')
|
||||
.requiredOption('--session <session>', 'Session ID')
|
||||
.requiredOption('--content <content>', 'Overview content')
|
||||
.action(storeOverview);
|
||||
|
||||
// Update session metadata command (for SDK streaming)
|
||||
program
|
||||
.command('update-session-metadata')
|
||||
.description('Update session title and subtitle (used by SDK)')
|
||||
.requiredOption('--project <project>', 'Project name')
|
||||
.requiredOption('--session <session>', 'Session ID')
|
||||
.requiredOption('--title <title>', 'Session title (3-6 words)')
|
||||
.option('--subtitle <subtitle>', 'Session subtitle (max 20 words)')
|
||||
.action(updateSessionMetadata);
|
||||
|
||||
// Changelog command
|
||||
program
|
||||
.command('changelog')
|
||||
.description('Generate CHANGELOG.md from claude-mem memories')
|
||||
.option('--historical <n>', 'Number of versions to search (default: current version only)')
|
||||
.option('--generate <version>', 'Generate changelog for a specific version')
|
||||
.option('--start <time>', 'Start time for memory search (ISO format)')
|
||||
.option('--end <time>', 'End time for memory search (ISO format)')
|
||||
.option('--update', 'Update CHANGELOG.md from JSONL entries')
|
||||
.option('--preview', 'Preview the generated changelog')
|
||||
.option('-v, --verbose', 'Show detailed output')
|
||||
.action(changelog);
|
||||
|
||||
// Generate title command
|
||||
program
|
||||
.command('generate-title <prompt>')
|
||||
.description('Generate a session title and subtitle from a prompt')
|
||||
.option('--json', 'Output as JSON')
|
||||
.option('--oneline', 'Output as single line (title - subtitle)')
|
||||
.option('--session-id <id>', 'Claude session ID to update')
|
||||
.option('--save', 'Save the generated title to the database (requires --session-id)')
|
||||
.action(generateTitle);
|
||||
|
||||
// </Block> =======================================
|
||||
|
||||
// <Block> 1.12 ===================================
|
||||
// Dynamic Chroma MCP Commands
|
||||
// Natural pattern: Register all Chroma MCP tools as CLI commands
|
||||
try {
|
||||
const chromaTools = loadChromaMCPTools();
|
||||
|
||||
for (const tool of chromaTools) {
|
||||
const cmd = program
|
||||
.command(tool.name)
|
||||
.description(tool.description || `Execute ${tool.name} MCP tool`);
|
||||
|
||||
// Add options from tool schema
|
||||
const options = generateCommandOptions(tool.inputSchema);
|
||||
for (const opt of options) {
|
||||
if (opt.required) {
|
||||
cmd.requiredOption(opt.flag, opt.description);
|
||||
} else {
|
||||
cmd.option(opt.flag, opt.description);
|
||||
}
|
||||
}
|
||||
|
||||
// Set action handler
|
||||
cmd.action(async (options: OptionValues) => {
|
||||
await executeChromaMCPTool(tool.name, options);
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Warning: Could not load Chroma MCP tools:', error);
|
||||
}
|
||||
// </Block> =======================================
|
||||
|
||||
// <Block> 1.11 ===================================
|
||||
// CLI Execution
|
||||
// Natural pattern: After defining all commands, parse and execute
|
||||
// Parse arguments and execute
|
||||
program.parse();
|
||||
// </Block> =======================================
|
||||
|
||||
// <Block> 1.12 ===================================
|
||||
// Module Exports for Programmatic Use
|
||||
// Export database and utility classes for hooks and external consumers
|
||||
export { DatabaseManager, StreamingSessionStore, migrations, initializeDatabase, getDatabase } from '../services/sqlite/index.js';
|
||||
// </Block> =======================================
|
||||
@@ -0,0 +1,744 @@
|
||||
import { OptionValues } from 'commander';
|
||||
import { query } from '@anthropic-ai/claude-agent-sdk';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { getClaudePath } from '../shared/settings.js';
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
interface ChangelogEntry {
|
||||
version: string;
|
||||
date: string;
|
||||
type: 'Added' | 'Changed' | 'Fixed' | 'Removed' | 'Deprecated' | 'Security';
|
||||
description: string;
|
||||
timestamp: string;
|
||||
generatedAt?: string; // When this changelog entry was created
|
||||
}
|
||||
|
||||
interface MemorySearchResult {
|
||||
version: string;
|
||||
text: string;
|
||||
metadata: any;
|
||||
}
|
||||
|
||||
export async function changelog(options: OptionValues): Promise<void> {
|
||||
try {
|
||||
// Handle --update flag to regenerate CHANGELOG.md from JSONL
|
||||
if (options.update) {
|
||||
await updateChangelogFromJsonl(options);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get current version and project name from package.json
|
||||
const packageJsonPath = path.join(process.cwd(), 'package.json');
|
||||
let currentVersion = 'unknown';
|
||||
let projectName = 'unknown';
|
||||
if (fs.existsSync(packageJsonPath)) {
|
||||
try {
|
||||
const packageData = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
||||
currentVersion = packageData.version || 'unknown';
|
||||
projectName = packageData.name || path.basename(process.cwd());
|
||||
} catch (e) {
|
||||
projectName = path.basename(process.cwd());
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate versions to search for based on flags
|
||||
const versionsToSearch: string[] = [];
|
||||
let historicalCount = options.historical || 1; // Default to current version only
|
||||
|
||||
// Handle --generate flag for specific version
|
||||
if (options.generate) {
|
||||
versionsToSearch.push(options.generate);
|
||||
historicalCount = 1; // Single version mode
|
||||
console.log(`🎯 Generating changelog for specific version: ${options.generate}`);
|
||||
} else if (currentVersion !== 'unknown') {
|
||||
// Normal mode: use current version or historical versions
|
||||
const parts = currentVersion.split('.');
|
||||
if (parts.length === 3) {
|
||||
let major = parseInt(parts[0]);
|
||||
let minor = parseInt(parts[1]);
|
||||
let patch = parseInt(parts[2]);
|
||||
|
||||
for (let i = 0; i < historicalCount; i++) {
|
||||
versionsToSearch.push(`${major}.${minor}.${patch}`);
|
||||
|
||||
// Decrement version
|
||||
if (patch === 0) {
|
||||
if (minor === 0) {
|
||||
// Can't go lower than x.0.0
|
||||
break;
|
||||
}
|
||||
minor--;
|
||||
patch = 9;
|
||||
} else {
|
||||
patch--;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (versionsToSearch.length === 0) {
|
||||
console.log('⚠️ Could not determine versions to search. Please check package.json');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Check if current version already has a changelog entry
|
||||
const projectChangelogDir = path.join(
|
||||
process.env.HOME || process.env.USERPROFILE || '',
|
||||
'.claude-mem',
|
||||
'projects'
|
||||
);
|
||||
const changelogJsonlPath = path.join(projectChangelogDir, `${projectName}-changelog.jsonl`);
|
||||
|
||||
let hasCurrentVersion = false;
|
||||
|
||||
if (fs.existsSync(changelogJsonlPath)) {
|
||||
const existingLines = fs.readFileSync(changelogJsonlPath, 'utf-8').split('\n').filter(l => l.trim());
|
||||
|
||||
for (const line of existingLines) {
|
||||
try {
|
||||
const entry = JSON.parse(line);
|
||||
if (entry.version === currentVersion) {
|
||||
hasCurrentVersion = true;
|
||||
}
|
||||
} catch (e) {
|
||||
// Skip invalid lines
|
||||
}
|
||||
}
|
||||
|
||||
if (!options.historical && !options.generate && historicalCount === 1) {
|
||||
if (hasCurrentVersion) {
|
||||
console.log(`❌ Version ${currentVersion} already has changelog entries.`);
|
||||
console.log('\n📝 Workflow:');
|
||||
console.log(' 1. Make your code updates');
|
||||
console.log(' 2. Build and test: bun run build');
|
||||
console.log(' 3. Bump version: npm version patch');
|
||||
console.log(' 4. Generate changelog: claude-mem changelog');
|
||||
console.log(' 5. Commit and push\n');
|
||||
console.log(`💡 Or use --historical 1 to regenerate this version's changelog`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get npm publish times for all versions we need
|
||||
let versionTimeRanges: Array<{version: string, startTime: string, endTime: string}> = [];
|
||||
|
||||
// Check if custom time range is provided
|
||||
if (options.start && options.end) {
|
||||
// Use custom time range for the specified version
|
||||
const version = options.generate || currentVersion;
|
||||
versionTimeRanges.push({
|
||||
version,
|
||||
startTime: options.start,
|
||||
endTime: options.end
|
||||
});
|
||||
|
||||
console.log(`📅 Using custom time range for ${version}:`);
|
||||
console.log(` Start: ${new Date(options.start).toLocaleString()}`);
|
||||
console.log(` End: ${new Date(options.end).toLocaleString()}`);
|
||||
} else {
|
||||
try {
|
||||
const npmTimeData = execSync(`npm view ${projectName} time --json`, {
|
||||
encoding: 'utf-8',
|
||||
timeout: 5000
|
||||
});
|
||||
const publishTimes = JSON.parse(npmTimeData);
|
||||
|
||||
// For historical mode, we need one extra previous version to get proper time ranges
|
||||
// E.g., for 3 versions, we need 4 timestamps to create 3 ranges
|
||||
let extraPrevVersion = '';
|
||||
if (historicalCount > 1) {
|
||||
// Get the version before our oldest version in the search list
|
||||
const oldestVersion = versionsToSearch[versionsToSearch.length - 1];
|
||||
const parts = oldestVersion.split('.');
|
||||
const major = parseInt(parts[0]);
|
||||
const minor = parseInt(parts[1]);
|
||||
const patch = parseInt(parts[2]);
|
||||
|
||||
if (patch > 0) {
|
||||
extraPrevVersion = `${major}.${minor}.${patch - 1}`;
|
||||
} else if (minor > 0) {
|
||||
// Look for highest patch of previous minor
|
||||
const prevMinorPrefix = `${major}.${minor - 1}.`;
|
||||
const prevMinorVersions = Object.keys(publishTimes)
|
||||
.filter(v => v.startsWith(prevMinorPrefix))
|
||||
.sort((a, b) => {
|
||||
const aPatch = parseInt(a.split('.')[2] || '0');
|
||||
const bPatch = parseInt(b.split('.')[2] || '0');
|
||||
return bPatch - aPatch;
|
||||
});
|
||||
if (prevMinorVersions.length > 0) {
|
||||
extraPrevVersion = prevMinorVersions[0];
|
||||
}
|
||||
} else if (major > 0) {
|
||||
// Look for highest version of previous major
|
||||
const prevMajorPrefix = `${major - 1}.`;
|
||||
const prevMajorVersions = Object.keys(publishTimes)
|
||||
.filter(v => v.startsWith(prevMajorPrefix))
|
||||
.sort((a, b) => {
|
||||
const [, aMinor, aPatch] = a.split('.').map(Number);
|
||||
const [, bMinor, bPatch] = b.split('.').map(Number);
|
||||
if (aMinor !== bMinor) return bMinor - aMinor;
|
||||
return bPatch - aPatch;
|
||||
});
|
||||
if (prevMajorVersions.length > 0) {
|
||||
extraPrevVersion = prevMajorVersions[0];
|
||||
}
|
||||
}
|
||||
|
||||
if (options.verbose && extraPrevVersion && publishTimes[extraPrevVersion]) {
|
||||
console.log(`📍 Using ${extraPrevVersion} as start boundary for time ranges`);
|
||||
}
|
||||
}
|
||||
|
||||
// Build time ranges for each version
|
||||
for (let i = 0; i < versionsToSearch.length; i++) {
|
||||
const version = versionsToSearch[i];
|
||||
|
||||
// Start time:
|
||||
// - For the first (newest) version, use the publish time of the version before it
|
||||
// - For middle versions, use the publish time of the next version in our list
|
||||
// - For the last (oldest) version, use the extra previous version we found
|
||||
let startTime = '2000-01-01T00:00:00Z'; // Default to old date
|
||||
|
||||
if (i === 0) {
|
||||
// First (newest) version - find its immediate predecessor
|
||||
const versionParts = version.split('.');
|
||||
const major = parseInt(versionParts[0]);
|
||||
const minor = parseInt(versionParts[1]);
|
||||
const patch = parseInt(versionParts[2]);
|
||||
|
||||
let prevVersion = '';
|
||||
if (patch > 0) {
|
||||
prevVersion = `${major}.${minor}.${patch - 1}`;
|
||||
} else if (minor > 0) {
|
||||
// Look for highest patch of previous minor
|
||||
const prevMinorPrefix = `${major}.${minor - 1}.`;
|
||||
const prevMinorVersions = Object.keys(publishTimes)
|
||||
.filter(v => v.startsWith(prevMinorPrefix))
|
||||
.sort((a, b) => {
|
||||
const aPatch = parseInt(a.split('.')[2] || '0');
|
||||
const bPatch = parseInt(b.split('.')[2] || '0');
|
||||
return bPatch - aPatch;
|
||||
});
|
||||
if (prevMinorVersions.length > 0) {
|
||||
prevVersion = prevMinorVersions[0];
|
||||
}
|
||||
}
|
||||
|
||||
if (publishTimes[prevVersion]) {
|
||||
startTime = publishTimes[prevVersion];
|
||||
}
|
||||
} else if (i < versionsToSearch.length - 1) {
|
||||
// Middle versions - use the next version in our list
|
||||
const prevVersionInList = versionsToSearch[i + 1];
|
||||
if (publishTimes[prevVersionInList]) {
|
||||
startTime = publishTimes[prevVersionInList];
|
||||
}
|
||||
} else {
|
||||
// Last (oldest) version - use the extra previous version
|
||||
if (extraPrevVersion && publishTimes[extraPrevVersion]) {
|
||||
startTime = publishTimes[extraPrevVersion];
|
||||
}
|
||||
}
|
||||
|
||||
// End time is this version's publish time (or now for unreleased)
|
||||
let endTime = publishTimes[version] || new Date().toISOString();
|
||||
|
||||
versionTimeRanges.push({ version, startTime, endTime });
|
||||
|
||||
if (options.verbose) {
|
||||
console.log(`📅 Version ${version}: ${new Date(startTime).toLocaleString()} - ${new Date(endTime).toLocaleString()}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Always log what we're doing for single version
|
||||
if (historicalCount === 1) {
|
||||
const latestRange = versionTimeRanges[0];
|
||||
if (latestRange) {
|
||||
console.log(`📦 Using npm time range for ${latestRange.version}: ${new Date(latestRange.startTime).toLocaleString()} - ${new Date(latestRange.endTime).toLocaleString()}`);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('❌ Could not fetch npm publish times. Cannot proceed without time ranges.');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`🔍 Searching memories for versions: ${versionsToSearch.join(', ')}`);
|
||||
console.log(`📦 Project: ${projectName}\n`);
|
||||
|
||||
// Phase 1: Search for version-related memories using MCP tools
|
||||
// ALWAYS use time range search - no other method
|
||||
const searchPrompt = versionTimeRanges.length > 0 ?
|
||||
`You are helping generate a changelog by searching for memories within specific time ranges for multiple versions.
|
||||
|
||||
PROJECT: ${projectName}
|
||||
VERSION TIME RANGES:
|
||||
${versionTimeRanges.map(r => `- Version ${r.version}: ${new Date(r.startTime).toLocaleDateString()} to ${new Date(r.endTime).toLocaleDateString()}`).join('\n')}
|
||||
|
||||
YOUR TASK:
|
||||
Use mcp__claude-mem__chroma_query_documents to search for memories for each version time range.
|
||||
|
||||
SEARCH STRATEGY:
|
||||
${versionTimeRanges.map(r => {
|
||||
const startDate = new Date(r.startTime);
|
||||
const endDate = new Date(r.endTime);
|
||||
|
||||
// Generate all date prefixes between start and end
|
||||
const datePrefixes: string[] = [];
|
||||
const currentDate = new Date(startDate);
|
||||
|
||||
while (currentDate <= endDate) {
|
||||
// Add day prefix like "2025-09-09"
|
||||
const dayPrefix = currentDate.toISOString().split('T')[0];
|
||||
datePrefixes.push(dayPrefix);
|
||||
currentDate.setDate(currentDate.getDate() + 1);
|
||||
}
|
||||
|
||||
return `
|
||||
Version ${r.version} (${new Date(r.startTime).toLocaleDateString()} to ${new Date(r.endTime).toLocaleDateString()}):
|
||||
1. Search for memories from these dates: ${datePrefixes.join(', ')}
|
||||
2. Make multiple calls to mcp__claude-mem__chroma_query_documents:
|
||||
- collection_name: "claude_memories"
|
||||
- query_texts: Include the project name AND date in each query:
|
||||
* "${projectName} ${datePrefixes[0]} feature"
|
||||
* "${projectName} ${datePrefixes[0]} fix"
|
||||
* "${projectName} ${datePrefixes[0]} change"
|
||||
* "${projectName} ${datePrefixes[0]} improvement"
|
||||
* "${projectName} ${datePrefixes[0]} refactor"
|
||||
- n_results: 50
|
||||
3. The date in the query text helps semantic search find memories from that day
|
||||
4. Assign memories to this version if their timestamp falls within:
|
||||
- Start: ${r.startTime}
|
||||
- End: ${r.endTime}`;
|
||||
}).join('\n')}
|
||||
|
||||
IMPORTANT:
|
||||
- Always include project name and date in query_texts for best results
|
||||
- Semantic search will naturally find memories near those dates
|
||||
- Group returned memories by version based on their timestamp metadata
|
||||
|
||||
Return a JSON object with this structure:
|
||||
{
|
||||
"memories": [
|
||||
{
|
||||
"version": "version_number",
|
||||
"text": "memory content",
|
||||
"metadata": {metadata object with timestamp},
|
||||
"relevance": "high/medium/low"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Group memories by the version they belong to based on timestamp.
|
||||
Start searching now.` :
|
||||
`ERROR: No time ranges available. This should never happen.`;
|
||||
|
||||
if (versionTimeRanges.length === 0) {
|
||||
console.log('❌ No time ranges available. Cannot search memories.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (options.verbose) {
|
||||
console.log('📝 Calling Claude to search memories...');
|
||||
}
|
||||
|
||||
// Call Claude with MCP tools to search memories
|
||||
const searchResponse = await query({
|
||||
prompt: searchPrompt,
|
||||
options: {
|
||||
allowedTools: [
|
||||
'mcp__claude-mem__chroma_query_documents',
|
||||
'mcp__claude-mem__chroma_get_documents'
|
||||
],
|
||||
pathToClaudeCodeExecutable: getClaudePath()
|
||||
}
|
||||
});
|
||||
|
||||
// Extract memories from response
|
||||
let memoriesJson = '';
|
||||
if (searchResponse && typeof searchResponse === 'object' && Symbol.asyncIterator in searchResponse) {
|
||||
for await (const message of searchResponse) {
|
||||
if (message?.type === 'assistant' && message?.message?.content) {
|
||||
const content = message.message.content;
|
||||
if (typeof content === 'string') {
|
||||
memoriesJson += content;
|
||||
} else if (Array.isArray(content)) {
|
||||
for (const block of content) {
|
||||
if (block.type === 'text' && block.text) {
|
||||
memoriesJson += block.text;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse memories
|
||||
let memories: MemorySearchResult[] = [];
|
||||
try {
|
||||
// Extract JSON from response (might be wrapped in markdown)
|
||||
const jsonMatch = memoriesJson.match(/```json\n([\s\S]*?)\n```/) ||
|
||||
memoriesJson.match(/\{[\s\S]*\}/);
|
||||
if (jsonMatch) {
|
||||
const parsed = JSON.parse(jsonMatch[1] || jsonMatch[0]);
|
||||
if (parsed.memories && Array.isArray(parsed.memories)) {
|
||||
memories = parsed.memories;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('⚠️ Could not parse memory search results:', e);
|
||||
}
|
||||
|
||||
if (memories.length === 0) {
|
||||
console.log('\n⚠️ No version-related memories found for this version.');
|
||||
console.log(' This is normal for the first release or when no changes were tracked.');
|
||||
console.log(' Creating a placeholder changelog entry...');
|
||||
|
||||
// Create a minimal placeholder entry
|
||||
const placeholderEntry: ChangelogEntry = {
|
||||
version: versionsToSearch[0], // Use the first (current) version
|
||||
date: todayStr,
|
||||
type: 'Changed',
|
||||
description: 'Initial release or minor updates',
|
||||
timestamp: new Date().toISOString(),
|
||||
generatedAt: new Date().toISOString()
|
||||
};
|
||||
|
||||
// Save the placeholder entry
|
||||
if (!fs.existsSync(projectChangelogDir)) {
|
||||
fs.mkdirSync(projectChangelogDir, { recursive: true });
|
||||
}
|
||||
|
||||
const jsonlContent = JSON.stringify(placeholderEntry) + '\n';
|
||||
fs.appendFileSync(changelogJsonlPath, jsonlContent);
|
||||
|
||||
console.log(`✅ Created placeholder changelog entry for v${versionsToSearch[0]}`);
|
||||
|
||||
// Generate the CHANGELOG.md with the placeholder
|
||||
await updateChangelogFromJsonl(options);
|
||||
|
||||
return; // Exit successfully
|
||||
}
|
||||
|
||||
console.log(`✅ Found ${memories.length} version-related memories\n`);
|
||||
|
||||
// Get system date for accuracy
|
||||
const systemDate = execSync('date "+%Y-%m-%d %H:%M:%S %Z"').toString().trim();
|
||||
const todayStr = systemDate.split(' ')[0]; // YYYY-MM-DD format
|
||||
|
||||
// Phase 2: Generate changelog entries from memories
|
||||
const changelogPrompt = `Analyze these memories and generate changelog entries.
|
||||
|
||||
PROJECT: ${projectName}
|
||||
DATE: ${todayStr}
|
||||
|
||||
MEMORIES BY VERSION:
|
||||
${versionsToSearch.map(version => {
|
||||
const versionMemories = memories.filter(m => m.version === version);
|
||||
if (versionMemories.length === 0) return `### Version ${version}\nNo memories found.`;
|
||||
return `### Version ${version} (${versionMemories.length} memories):
|
||||
${versionMemories.map((m, i) => `${i + 1}. ${m.text}`).join('\n')}`;
|
||||
}).join('\n\n')}
|
||||
|
||||
INSTRUCTIONS:
|
||||
1. Extract concrete changes, fixes, and additions from the memories
|
||||
2. Categorize each change as: Added, Changed, Fixed, Removed, Deprecated, or Security
|
||||
3. Write clear, user-facing descriptions
|
||||
4. Start each entry with an action verb
|
||||
5. Focus on what matters to users, not internal implementation details
|
||||
|
||||
Return ONLY a JSON array with this structure:
|
||||
[
|
||||
{
|
||||
"version": "3.6.1",
|
||||
"type": "Added",
|
||||
"description": "New feature description"
|
||||
},
|
||||
{
|
||||
"version": "3.6.1",
|
||||
"type": "Fixed",
|
||||
"description": "Bug fix description"
|
||||
}
|
||||
]`;
|
||||
|
||||
console.log('🔄 Generating changelog entries...');
|
||||
|
||||
// Call Claude to generate changelog entries
|
||||
const changelogResponse = await query({
|
||||
prompt: changelogPrompt,
|
||||
options: {
|
||||
allowedTools: [],
|
||||
pathToClaudeCodeExecutable: getClaudePath()
|
||||
}
|
||||
});
|
||||
|
||||
// Extract JSON from response
|
||||
let entriesJson = '';
|
||||
if (changelogResponse && typeof changelogResponse === 'object' && Symbol.asyncIterator in changelogResponse) {
|
||||
for await (const message of changelogResponse) {
|
||||
if (message?.type === 'assistant' && message?.message?.content) {
|
||||
const content = message.message.content;
|
||||
if (typeof content === 'string') {
|
||||
entriesJson += content;
|
||||
} else if (Array.isArray(content)) {
|
||||
for (const block of content) {
|
||||
if (block.type === 'text' && block.text) {
|
||||
entriesJson += block.text;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse changelog entries
|
||||
let entries: ChangelogEntry[] = [];
|
||||
try {
|
||||
// Extract JSON (might be wrapped in markdown)
|
||||
const jsonMatch = entriesJson.match(/```json\n([\s\S]*?)\n```/) ||
|
||||
entriesJson.match(/\[[\s\S]*\]/);
|
||||
if (jsonMatch) {
|
||||
const parsed = JSON.parse(jsonMatch[1] || jsonMatch[0]);
|
||||
if (Array.isArray(parsed)) {
|
||||
const generatedAt = new Date().toISOString();
|
||||
entries = parsed.map(e => ({
|
||||
...e,
|
||||
date: todayStr,
|
||||
timestamp: e.timestamp || generatedAt, // Memory timestamp if available
|
||||
generatedAt: generatedAt // When this changelog was generated
|
||||
}));
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('⚠️ Could not parse changelog entries:', e);
|
||||
}
|
||||
|
||||
if (entries.length === 0) {
|
||||
console.log('⚠️ No changelog entries generated.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Ensure project changelog directory exists
|
||||
if (!fs.existsSync(projectChangelogDir)) {
|
||||
fs.mkdirSync(projectChangelogDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Save entries to project JSONL file
|
||||
console.log(`\n💾 Saving ${entries.length} changelog entries to ${path.basename(changelogJsonlPath)}`);
|
||||
|
||||
// When using --historical or --generate, remove old entries for the versions being regenerated
|
||||
if ((options.historical && historicalCount > 1) || options.generate) {
|
||||
let existingEntries: ChangelogEntry[] = [];
|
||||
if (fs.existsSync(changelogJsonlPath)) {
|
||||
const lines = fs.readFileSync(changelogJsonlPath, 'utf-8').split('\n').filter(l => l.trim());
|
||||
for (const line of lines) {
|
||||
try {
|
||||
const entry = JSON.parse(line);
|
||||
// Keep entries that are NOT in the versions we're regenerating
|
||||
if (!versionsToSearch.includes(entry.version)) {
|
||||
existingEntries.push(entry);
|
||||
}
|
||||
} catch (e) {
|
||||
// Skip invalid lines
|
||||
}
|
||||
}
|
||||
}
|
||||
// Rewrite the file with filtered entries plus new ones
|
||||
const allEntries = [...existingEntries, ...entries];
|
||||
const jsonlContent = allEntries.map(entry => JSON.stringify(entry)).join('\n') + '\n';
|
||||
fs.writeFileSync(changelogJsonlPath, jsonlContent);
|
||||
console.log(`🔄 Regenerated entries for versions: ${versionsToSearch.join(', ')}`);
|
||||
} else {
|
||||
// Append new entries to JSONL
|
||||
const jsonlContent = entries.map(entry => JSON.stringify(entry)).join('\n') + '\n';
|
||||
fs.appendFileSync(changelogJsonlPath, jsonlContent);
|
||||
}
|
||||
|
||||
// Now generate markdown from all JSONL entries
|
||||
console.log('\n📝 Generating CHANGELOG.md from entries...');
|
||||
|
||||
// Read all entries from JSONL
|
||||
let allEntries: ChangelogEntry[] = [];
|
||||
if (fs.existsSync(changelogJsonlPath)) {
|
||||
const lines = fs.readFileSync(changelogJsonlPath, 'utf-8').split('\n').filter(l => l.trim());
|
||||
for (const line of lines) {
|
||||
try {
|
||||
allEntries.push(JSON.parse(line));
|
||||
} catch (e) {
|
||||
// Skip invalid lines
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Group entries by version
|
||||
const entriesByVersion = new Map<string, ChangelogEntry[]>();
|
||||
for (const entry of allEntries) {
|
||||
if (!entriesByVersion.has(entry.version)) {
|
||||
entriesByVersion.set(entry.version, []);
|
||||
}
|
||||
entriesByVersion.get(entry.version)!.push(entry);
|
||||
}
|
||||
|
||||
// Generate markdown
|
||||
let markdown = '# Changelog\n\nAll notable changes to this project will be documented in this file.\n\nThe format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).\n\n';
|
||||
|
||||
// Sort versions in descending order
|
||||
const sortedVersions = Array.from(entriesByVersion.keys()).sort((a, b) => {
|
||||
const aParts = a.split('.').map(Number);
|
||||
const bParts = b.split('.').map(Number);
|
||||
for (let i = 0; i < 3; i++) {
|
||||
if (aParts[i] !== bParts[i]) return bParts[i] - aParts[i];
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
for (const version of sortedVersions) {
|
||||
const versionEntries = entriesByVersion.get(version)!;
|
||||
const date = versionEntries[0].date || todayStr;
|
||||
|
||||
markdown += `\n## [${version}] - ${date}\n\n`;
|
||||
|
||||
// Group by type
|
||||
const types: Array<ChangelogEntry['type']> = ['Added', 'Changed', 'Fixed', 'Removed', 'Deprecated', 'Security'];
|
||||
for (const type of types) {
|
||||
const typeEntries = versionEntries.filter(e => e.type === type);
|
||||
if (typeEntries.length > 0) {
|
||||
markdown += `### ${type}\n`;
|
||||
for (const entry of typeEntries) {
|
||||
markdown += `- ${entry.description}\n`;
|
||||
}
|
||||
markdown += '\n';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Write the CHANGELOG.md
|
||||
const changelogPath = path.join(process.cwd(), 'CHANGELOG.md');
|
||||
fs.writeFileSync(changelogPath, markdown);
|
||||
|
||||
console.log(`✅ Generated CHANGELOG.md with ${allEntries.length} total entries across ${entriesByVersion.size} versions!`);
|
||||
|
||||
if (options.preview) {
|
||||
console.log('\n📄 Preview:\n');
|
||||
console.log(markdown.split('\n').slice(0, 30).join('\n'));
|
||||
if (markdown.split('\n').length > 30) {
|
||||
console.log('\n... (truncated for preview)');
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Error generating changelog:', error instanceof Error ? error.message : error);
|
||||
if (error instanceof Error && error.stack) {
|
||||
console.error('Stack:', error.stack);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
async function updateChangelogFromJsonl(options: OptionValues): Promise<void> {
|
||||
try {
|
||||
// Get project name from package.json
|
||||
const packageJsonPath = path.join(process.cwd(), 'package.json');
|
||||
let projectName = 'unknown';
|
||||
if (fs.existsSync(packageJsonPath)) {
|
||||
try {
|
||||
const packageData = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
||||
projectName = packageData.name || path.basename(process.cwd());
|
||||
} catch (e) {
|
||||
projectName = path.basename(process.cwd());
|
||||
}
|
||||
}
|
||||
|
||||
const projectChangelogDir = path.join(
|
||||
process.env.HOME || process.env.USERPROFILE || '',
|
||||
'.claude-mem',
|
||||
'projects'
|
||||
);
|
||||
const changelogJsonlPath = path.join(projectChangelogDir, `${projectName}-changelog.jsonl`);
|
||||
|
||||
if (!fs.existsSync(changelogJsonlPath)) {
|
||||
console.log('❌ No changelog entries found. Generate some first with: claude-mem changelog');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('📝 Updating CHANGELOG.md from JSONL entries...');
|
||||
|
||||
// Read all entries from JSONL
|
||||
let allEntries: ChangelogEntry[] = [];
|
||||
const lines = fs.readFileSync(changelogJsonlPath, 'utf-8').split('\n').filter(l => l.trim());
|
||||
for (const line of lines) {
|
||||
try {
|
||||
allEntries.push(JSON.parse(line));
|
||||
} catch (e) {
|
||||
// Skip invalid lines
|
||||
}
|
||||
}
|
||||
|
||||
if (allEntries.length === 0) {
|
||||
console.log('❌ No valid entries found in JSONL file');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Group entries by version
|
||||
const entriesByVersion = new Map<string, ChangelogEntry[]>();
|
||||
for (const entry of allEntries) {
|
||||
if (!entriesByVersion.has(entry.version)) {
|
||||
entriesByVersion.set(entry.version, []);
|
||||
}
|
||||
entriesByVersion.get(entry.version)!.push(entry);
|
||||
}
|
||||
|
||||
// Generate markdown
|
||||
let markdown = '# Changelog\n\nAll notable changes to this project will be documented in this file.\n\nThe format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).\n\n';
|
||||
|
||||
// Sort versions in descending order
|
||||
const sortedVersions = Array.from(entriesByVersion.keys()).sort((a, b) => {
|
||||
const aParts = a.split('.').map(Number);
|
||||
const bParts = b.split('.').map(Number);
|
||||
for (let i = 0; i < 3; i++) {
|
||||
if (aParts[i] !== bParts[i]) return bParts[i] - aParts[i];
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
for (const version of sortedVersions) {
|
||||
const versionEntries = entriesByVersion.get(version)!;
|
||||
const date = versionEntries[0].date;
|
||||
|
||||
markdown += `\n## [${version}] - ${date}\n\n`;
|
||||
|
||||
// Group by type
|
||||
const types: Array<ChangelogEntry['type']> = ['Added', 'Changed', 'Fixed', 'Removed', 'Deprecated', 'Security'];
|
||||
for (const type of types) {
|
||||
const typeEntries = versionEntries.filter(e => e.type === type);
|
||||
if (typeEntries.length > 0) {
|
||||
markdown += `### ${type}\n`;
|
||||
for (const entry of typeEntries) {
|
||||
markdown += `- ${entry.description}\n`;
|
||||
}
|
||||
markdown += '\n';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Write the CHANGELOG.md
|
||||
const changelogPath = path.join(process.cwd(), 'CHANGELOG.md');
|
||||
fs.writeFileSync(changelogPath, markdown);
|
||||
|
||||
console.log(`✅ Updated CHANGELOG.md with ${allEntries.length} entries across ${entriesByVersion.size} versions!`);
|
||||
|
||||
if (options.preview) {
|
||||
console.log('\n📄 Preview:\n');
|
||||
console.log(markdown.split('\n').slice(0, 30).join('\n'));
|
||||
if (markdown.split('\n').length > 30) {
|
||||
console.log('\n... (truncated for preview)');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Error updating changelog:', error instanceof Error ? error.message : error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
import { OptionValues } from 'commander';
|
||||
import ChromaMCPClient from '../../chroma-mcp-tools/chroma-mcp-client.js';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
/**
|
||||
* Generic Chroma MCP tool executor
|
||||
* Dynamically calls any Chroma MCP tool with provided arguments
|
||||
*/
|
||||
export async function executeChromaMCPTool(toolName: string, options: OptionValues): Promise<void> {
|
||||
const client = new ChromaMCPClient();
|
||||
|
||||
try {
|
||||
await client.connect();
|
||||
|
||||
// Convert commander options to tool arguments
|
||||
const toolArgs = convertOptionsToArgs(toolName, options);
|
||||
|
||||
// Call the MCP tool
|
||||
const result = await client.callTool(toolName, toolArgs);
|
||||
|
||||
// Parse and format the result nicely
|
||||
const formatted = formatMCPResult(result);
|
||||
console.log(formatted);
|
||||
|
||||
await client.disconnect();
|
||||
process.exit(0);
|
||||
} catch (error: any) {
|
||||
console.error(JSON.stringify({
|
||||
success: false,
|
||||
error: error.message || 'Unknown error calling MCP tool',
|
||||
tool: toolName
|
||||
}, null, 2));
|
||||
|
||||
await client.disconnect();
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format MCP tool result for clean CLI output
|
||||
*/
|
||||
function formatMCPResult(result: any): string {
|
||||
// If result has content array (MCP protocol format)
|
||||
if (result?.content && Array.isArray(result.content)) {
|
||||
const textContent = result.content
|
||||
.filter((item: any) => item.type === 'text')
|
||||
.map((item: any) => item.text)
|
||||
.join('\n');
|
||||
|
||||
// Try to parse as JSON for prettier output
|
||||
try {
|
||||
const parsed = JSON.parse(textContent);
|
||||
return JSON.stringify(parsed, null, 2);
|
||||
} catch {
|
||||
// Not JSON, return as-is
|
||||
return textContent;
|
||||
}
|
||||
}
|
||||
|
||||
// If result is already an object, pretty print it
|
||||
if (typeof result === 'object') {
|
||||
return JSON.stringify(result, null, 2);
|
||||
}
|
||||
|
||||
// Fallback to string
|
||||
return String(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert CLI options to MCP tool arguments
|
||||
* Handles type conversion and array parsing
|
||||
*/
|
||||
function convertOptionsToArgs(toolName: string, options: OptionValues): Record<string, any> {
|
||||
const args: Record<string, any> = {};
|
||||
|
||||
for (const [key, value] of Object.entries(options)) {
|
||||
// Skip commander internal properties
|
||||
if (key.startsWith('_') || typeof value === 'function') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Try to parse JSON strings
|
||||
if (typeof value === 'string') {
|
||||
try {
|
||||
args[key] = JSON.parse(value);
|
||||
} catch {
|
||||
args[key] = value;
|
||||
}
|
||||
} else {
|
||||
args[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load Chroma MCP tool definitions from JSON
|
||||
*/
|
||||
export function loadChromaMCPTools(): Array<{
|
||||
name: string;
|
||||
description: string;
|
||||
inputSchema: any;
|
||||
}> {
|
||||
// Try multiple path resolutions for dev vs production
|
||||
const possiblePaths = [
|
||||
path.join(__dirname, '../../chroma-mcp-tools/CHROMA_MCP_TOOLS.json'),
|
||||
path.join(process.cwd(), 'chroma-mcp-tools/CHROMA_MCP_TOOLS.json'),
|
||||
path.join(__dirname, '../chroma-mcp-tools/CHROMA_MCP_TOOLS.json')
|
||||
];
|
||||
|
||||
for (const toolsPath of possiblePaths) {
|
||||
if (fs.existsSync(toolsPath)) {
|
||||
const toolsJson = fs.readFileSync(toolsPath, 'utf-8');
|
||||
return JSON.parse(toolsJson);
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('Could not find CHROMA_MCP_TOOLS.json');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate CLI command options from MCP tool schema
|
||||
*/
|
||||
export function generateCommandOptions(schema: any): Array<{
|
||||
flag: string;
|
||||
description: string;
|
||||
required: boolean;
|
||||
type: string;
|
||||
}> {
|
||||
const options: Array<{
|
||||
flag: string;
|
||||
description: string;
|
||||
required: boolean;
|
||||
type: string;
|
||||
}> = [];
|
||||
|
||||
if (!schema.properties) return options;
|
||||
|
||||
const required = schema.required || [];
|
||||
|
||||
for (const [propName, propSchema] of Object.entries(schema.properties)) {
|
||||
const prop = propSchema as any;
|
||||
const isRequired = required.includes(propName);
|
||||
|
||||
// Determine type
|
||||
let type = 'string';
|
||||
if (prop.type === 'integer' || prop.type === 'number') {
|
||||
type = 'number';
|
||||
} else if (prop.type === 'array') {
|
||||
type = 'array';
|
||||
} else if (prop.type === 'object') {
|
||||
type = 'json';
|
||||
} else if (prop.anyOf) {
|
||||
// Handle nullable types
|
||||
const nonNullType = prop.anyOf.find((t: any) => t.type !== 'null');
|
||||
if (nonNullType?.type === 'integer' || nonNullType?.type === 'number') {
|
||||
type = 'number';
|
||||
} else if (nonNullType?.type === 'array') {
|
||||
type = 'array';
|
||||
} else if (nonNullType?.type === 'object') {
|
||||
type = 'json';
|
||||
}
|
||||
}
|
||||
|
||||
// Build flag
|
||||
const flag = isRequired
|
||||
? `--${propName} <${type}>`
|
||||
: `--${propName} [${type}]`;
|
||||
|
||||
// Build description
|
||||
let description = prop.title || propName;
|
||||
if (prop.default !== undefined) {
|
||||
description += ` (default: ${JSON.stringify(prop.default)})`;
|
||||
}
|
||||
if (type === 'array') {
|
||||
description += ' (JSON array)';
|
||||
} else if (type === 'json') {
|
||||
description += ' (JSON object)';
|
||||
}
|
||||
|
||||
options.push({
|
||||
flag,
|
||||
description,
|
||||
required: isRequired,
|
||||
type
|
||||
});
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import { OptionValues } from 'commander';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { PathDiscovery } from '../services/path-discovery.js';
|
||||
import { createStores } from '../services/sqlite/index.js';
|
||||
import { rollingLog } from '../shared/rolling-log.js';
|
||||
|
||||
type CheckStatus = 'pass' | 'fail' | 'warn';
|
||||
|
||||
interface CheckResult {
|
||||
name: string;
|
||||
status: CheckStatus;
|
||||
details?: string;
|
||||
}
|
||||
|
||||
function printCheck(result: CheckResult): void {
|
||||
const icon =
|
||||
result.status === 'pass' ? '✅' : result.status === 'warn' ? '⚠️ ' : '❌';
|
||||
const message = result.details ? `${result.name}: ${result.details}` : result.name;
|
||||
console.log(`${icon} ${message}`);
|
||||
}
|
||||
|
||||
export async function doctor(options: OptionValues = {}): Promise<void> {
|
||||
const discovery = PathDiscovery.getInstance();
|
||||
const checks: CheckResult[] = [];
|
||||
|
||||
// Data directory
|
||||
try {
|
||||
const dataDir = discovery.getDataDirectory();
|
||||
if (!fs.existsSync(dataDir)) {
|
||||
fs.mkdirSync(dataDir, { recursive: true });
|
||||
checks.push({ name: `Data directory created at ${dataDir}`, status: 'warn' });
|
||||
} else {
|
||||
const stats = fs.statSync(dataDir);
|
||||
let writable = false;
|
||||
try {
|
||||
fs.accessSync(dataDir, fs.constants.W_OK);
|
||||
writable = true;
|
||||
} catch {}
|
||||
checks.push({
|
||||
name: `Data directory ${dataDir}`,
|
||||
status: stats.isDirectory() && writable ? 'pass' : 'fail',
|
||||
details: stats.isDirectory() && writable ? 'accessible' : 'not writable'
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
checks.push({
|
||||
name: 'Data directory',
|
||||
status: 'fail',
|
||||
details: error?.message || String(error)
|
||||
});
|
||||
}
|
||||
|
||||
// SQLite connectivity
|
||||
let stores; // reuse for queue check
|
||||
try {
|
||||
stores = await createStores();
|
||||
const sessionCount = stores.sessions.count();
|
||||
checks.push({
|
||||
name: 'SQLite database',
|
||||
status: 'pass',
|
||||
details: `${sessionCount} session${sessionCount === 1 ? '' : 's'} present`
|
||||
});
|
||||
} catch (error: any) {
|
||||
checks.push({
|
||||
name: 'SQLite database',
|
||||
status: 'fail',
|
||||
details: error?.message || String(error)
|
||||
});
|
||||
}
|
||||
|
||||
// Chroma connectivity
|
||||
try {
|
||||
const chromaDir = discovery.getChromaDirectory();
|
||||
const chromaExists = fs.existsSync(chromaDir);
|
||||
checks.push({
|
||||
name: 'Chroma vector store',
|
||||
status: chromaExists ? 'pass' : 'warn',
|
||||
details: chromaExists ? `data dir ${path.resolve(chromaDir)}` : 'Not yet initialized'
|
||||
});
|
||||
} catch (error: any) {
|
||||
checks.push({
|
||||
name: 'Chroma vector store',
|
||||
status: 'warn',
|
||||
details: error?.message || 'Unable to check Chroma directory'
|
||||
});
|
||||
}
|
||||
|
||||
if (options.json) {
|
||||
console.log(JSON.stringify({ checks }, null, 2));
|
||||
} else {
|
||||
console.log('claude-mem doctor');
|
||||
console.log('=================');
|
||||
checks.forEach(printCheck);
|
||||
}
|
||||
|
||||
rollingLog('info', 'doctor run completed', {
|
||||
status: checks.map((c) => c.status)
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
import { OptionValues } from 'commander';
|
||||
import { query } from '@anthropic-ai/claude-agent-sdk';
|
||||
import { getClaudePath } from '../shared/settings.js';
|
||||
import { DatabaseManager } from '../services/sqlite/Database.js';
|
||||
import { StreamingSessionStore } from '../services/sqlite/StreamingSessionStore.js';
|
||||
import { migrations } from '../services/sqlite/migrations.js';
|
||||
|
||||
/**
|
||||
* Generate a session title and subtitle from a user prompt
|
||||
* CLI command that uses Agent SDK (like changelog.ts)
|
||||
*
|
||||
* Can be called in two modes:
|
||||
* 1. Standalone: generate-title "user prompt" --json
|
||||
* 2. With session: generate-title "user prompt" --session-id <id> --save
|
||||
*/
|
||||
export async function generateTitle(prompt: string, options: OptionValues): Promise<void> {
|
||||
if (!prompt || prompt.trim().length === 0) {
|
||||
console.error(JSON.stringify({
|
||||
success: false,
|
||||
error: 'Prompt is required'
|
||||
}));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// If --session-id provided, validate that session exists
|
||||
let streamingStore: StreamingSessionStore | null = null;
|
||||
let sessionRecord = null;
|
||||
|
||||
if (options.sessionId) {
|
||||
try {
|
||||
const dbManager = DatabaseManager.getInstance();
|
||||
for (const migration of migrations) {
|
||||
dbManager.registerMigration(migration);
|
||||
}
|
||||
const db = await dbManager.initialize();
|
||||
streamingStore = new StreamingSessionStore(db);
|
||||
|
||||
sessionRecord = streamingStore.getByClaudeSessionId(options.sessionId);
|
||||
if (!sessionRecord) {
|
||||
console.error(JSON.stringify({
|
||||
success: false,
|
||||
error: `Session not found: ${options.sessionId}`
|
||||
}));
|
||||
process.exit(1);
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(JSON.stringify({
|
||||
success: false,
|
||||
error: `Database error: ${error.message}`
|
||||
}));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
const systemPrompt = `You are a title and subtitle generator for claude-mem session metadata.
|
||||
|
||||
Your job is to analyze a user's request and generate:
|
||||
1. A concise title (3-8 words)
|
||||
2. A one-sentence subtitle (max 20 words)
|
||||
|
||||
TITLE GUIDELINES:
|
||||
- 3-8 words maximum
|
||||
- Scannable and clear
|
||||
- Captures the core action or topic
|
||||
- Professional and informative
|
||||
- Examples:
|
||||
* "Dark Mode Implementation"
|
||||
* "Authentication Bug Fix"
|
||||
* "API Rate Limiting Setup"
|
||||
* "React Component Refactoring"
|
||||
|
||||
SUBTITLE GUIDELINES:
|
||||
- One sentence, max 20 words
|
||||
- Descriptive and specific
|
||||
- Focus on the outcome or benefit
|
||||
- Use active voice when possible
|
||||
- Examples:
|
||||
* "Adding theme toggle and dark color scheme support to the application"
|
||||
* "Resolving login timeout issue affecting user session persistence"
|
||||
* "Implementing request throttling to prevent API quota exhaustion"
|
||||
|
||||
OUTPUT FORMAT:
|
||||
You must output EXACTLY two lines:
|
||||
Line 1: Title only (no prefix, no quotes)
|
||||
Line 2: Subtitle only (no prefix, no quotes)
|
||||
|
||||
EXAMPLE:
|
||||
|
||||
User request: "Help me add dark mode to my app"
|
||||
|
||||
Output:
|
||||
Dark Mode Implementation
|
||||
Adding theme toggle and dark color scheme support to the application
|
||||
|
||||
USER REQUEST:
|
||||
${prompt}
|
||||
|
||||
Now generate the title and subtitle (two lines exactly):`;
|
||||
|
||||
try {
|
||||
const response = await query({
|
||||
prompt: systemPrompt,
|
||||
options: {
|
||||
allowedTools: [],
|
||||
pathToClaudeCodeExecutable: getClaudePath()
|
||||
}
|
||||
});
|
||||
|
||||
// Extract text from response (same pattern as changelog.ts)
|
||||
let fullResponse = '';
|
||||
if (response && typeof response === 'object' && Symbol.asyncIterator in response) {
|
||||
for await (const message of response) {
|
||||
if (message?.type === 'assistant' && message?.message?.content) {
|
||||
const content = message.message.content;
|
||||
if (typeof content === 'string') {
|
||||
fullResponse += content;
|
||||
} else if (Array.isArray(content)) {
|
||||
for (const block of content) {
|
||||
if (block.type === 'text' && block.text) {
|
||||
fullResponse += block.text;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse the response - expecting exactly 2 lines
|
||||
const lines = fullResponse.trim().split('\n').filter(line => line.trim().length > 0);
|
||||
|
||||
if (lines.length < 2) {
|
||||
console.error(JSON.stringify({
|
||||
success: false,
|
||||
error: 'Could not generate title and subtitle',
|
||||
response: fullResponse
|
||||
}));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const title = lines[0].trim();
|
||||
const subtitle = lines[1].trim();
|
||||
|
||||
// If --save and we have a session, update the database
|
||||
if (options.save && streamingStore && sessionRecord) {
|
||||
try {
|
||||
streamingStore.update(sessionRecord.id, {
|
||||
title,
|
||||
subtitle
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error(JSON.stringify({
|
||||
success: false,
|
||||
error: `Failed to save title: ${error.message}`
|
||||
}));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Output format depends on options
|
||||
if (options.json) {
|
||||
console.log(JSON.stringify({
|
||||
success: true,
|
||||
title,
|
||||
subtitle,
|
||||
sessionId: sessionRecord?.claude_session_id
|
||||
}, null, 2));
|
||||
} else if (options.oneline) {
|
||||
console.log(`${title} - ${subtitle}`);
|
||||
} else {
|
||||
console.log(title);
|
||||
console.log(subtitle);
|
||||
}
|
||||
|
||||
} catch (error: any) {
|
||||
console.error(JSON.stringify({
|
||||
success: false,
|
||||
error: error.message || 'Unknown error generating title'
|
||||
}));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,604 @@
|
||||
import { OptionValues } from 'commander';
|
||||
import { readFileSync, writeFileSync, existsSync, mkdirSync, copyFileSync, statSync, readdirSync } from 'fs';
|
||||
import { join, resolve, dirname } from 'path';
|
||||
import { homedir } from 'os';
|
||||
import { execSync } from 'child_process';
|
||||
import { fileURLToPath } from 'url';
|
||||
import * as p from '@clack/prompts';
|
||||
import gradient from 'gradient-string';
|
||||
import chalk from 'chalk';
|
||||
import boxen from 'boxen';
|
||||
import { PACKAGE_NAME } from '../shared/config.js';
|
||||
import type { Settings } from '../shared/types.js';
|
||||
import { PathDiscovery } from '../services/path-discovery.js';
|
||||
import { Platform } from '../utils/platform.js';
|
||||
|
||||
|
||||
// Enhanced animation utilities
|
||||
function createLoadingAnimation(message: string) {
|
||||
let interval: NodeJS.Timeout;
|
||||
let frame = 0;
|
||||
const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
||||
|
||||
return {
|
||||
start() {
|
||||
interval = setInterval(() => {
|
||||
process.stdout.write(`\r${chalk.cyan(frames[frame % frames.length])} ${message}`);
|
||||
frame++;
|
||||
}, 50); // Faster spinner animation (was 80ms)
|
||||
},
|
||||
stop(result: string, success: boolean = true) {
|
||||
clearInterval(interval);
|
||||
const icon = success ? chalk.green('✓') : chalk.red('✗');
|
||||
process.stdout.write(`\r${icon} ${result}\n`);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Create animated rainbow text with adjustable speed
|
||||
function animatedRainbow(text: string, speed: number = 100): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
let offset = 0;
|
||||
const maxFrames = 10;
|
||||
|
||||
const interval = setInterval(() => {
|
||||
// Create a shifted gradient by rotating through different presets
|
||||
const gradients = [fastRainbow, vibrantRainbow, gradient.rainbow, gradient.pastel];
|
||||
const shifted = gradients[offset % gradients.length](text);
|
||||
process.stdout.write('\r' + shifted);
|
||||
offset++;
|
||||
|
||||
if (offset >= maxFrames) {
|
||||
clearInterval(interval);
|
||||
resolve();
|
||||
}
|
||||
}, speed);
|
||||
});
|
||||
}
|
||||
|
||||
// Sleep utility for smooth animations
|
||||
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
|
||||
|
||||
// Fast rainbow gradient preset with tighter color transitions
|
||||
const fastRainbow = gradient(['#ff0000', '#ff4500', '#ffa500', '#ffff00', '#00ff00', '#00ffff', '#0000ff', '#8b00ff']);
|
||||
const vibrantRainbow = gradient(['#ff006e', '#fb5607', '#ffbe0b', '#8338ec', '#3a86ff']);
|
||||
|
||||
// Installation scope type
|
||||
type InstallScope = 'user' | 'project' | 'local';
|
||||
|
||||
// Installation configuration from wizard
|
||||
interface InstallConfig {
|
||||
scope: InstallScope;
|
||||
customPath?: string;
|
||||
hookTimeout: number;
|
||||
forceReinstall: boolean;
|
||||
enableSmartTrash?: boolean;
|
||||
saveMemoriesOnClear?: boolean;
|
||||
}
|
||||
|
||||
|
||||
function installUv(): void {
|
||||
Platform.installUv();
|
||||
process.env.PATH = `${homedir()}/.cargo/bin:${process.env.PATH}`;
|
||||
}
|
||||
|
||||
function detectClaudePath(): string {
|
||||
return Platform.findExecutable('claude');
|
||||
}
|
||||
|
||||
function hasExistingInstallation(): boolean {
|
||||
const pathDiscovery = PathDiscovery.getInstance();
|
||||
return existsSync(pathDiscovery.getHooksDirectory());
|
||||
}
|
||||
|
||||
async function runInstallationWizard(existingInstall: boolean): Promise<InstallConfig | null> {
|
||||
const config: Partial<InstallConfig> = {};
|
||||
|
||||
if (existingInstall) {
|
||||
const shouldReinstall = await p.confirm({
|
||||
message: '🧠 Existing claude-mem installation detected. Your memories and data are safe!\n\nReinstall to update hooks and configuration?',
|
||||
initialValue: true
|
||||
});
|
||||
|
||||
if (p.isCancel(shouldReinstall) || !shouldReinstall) {
|
||||
p.cancel('Installation cancelled');
|
||||
return null;
|
||||
}
|
||||
|
||||
config.forceReinstall = true;
|
||||
} else {
|
||||
config.forceReinstall = false;
|
||||
}
|
||||
|
||||
// Select installation scope
|
||||
const scope = await p.select({
|
||||
message: 'Select installation scope',
|
||||
options: [
|
||||
{
|
||||
value: 'user',
|
||||
label: 'User (Recommended)',
|
||||
hint: 'Install for current user (~/.claude)'
|
||||
},
|
||||
{
|
||||
value: 'project',
|
||||
label: 'Project',
|
||||
hint: 'Install for current project only (./.mcp.json)'
|
||||
},
|
||||
{
|
||||
value: 'local',
|
||||
label: 'Local',
|
||||
hint: 'Custom local installation'
|
||||
}
|
||||
],
|
||||
initialValue: 'user'
|
||||
});
|
||||
|
||||
if (p.isCancel(scope)) {
|
||||
p.cancel('Installation cancelled');
|
||||
return null;
|
||||
}
|
||||
|
||||
config.scope = scope as InstallScope;
|
||||
|
||||
// If local scope, ask for custom path
|
||||
if (scope === 'local') {
|
||||
const customPath = await p.text({
|
||||
message: 'Enter custom installation directory',
|
||||
placeholder: join(process.cwd(), '.claude-mem'),
|
||||
validate: (value) => {
|
||||
if (!value) return 'Path is required';
|
||||
if (!value.startsWith('/') && !value.startsWith('~')) {
|
||||
return 'Please provide an absolute path';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (p.isCancel(customPath)) {
|
||||
p.cancel('Installation cancelled');
|
||||
return null;
|
||||
}
|
||||
|
||||
config.customPath = customPath as string;
|
||||
}
|
||||
|
||||
// Use default hook timeout (3 minutes)
|
||||
config.hookTimeout = 180000;
|
||||
|
||||
// Always install/reinstall Chroma MCP - it's required for claude-mem to work
|
||||
|
||||
// Ask about smart trash alias
|
||||
const enableSmartTrash = await p.confirm({
|
||||
message: 'Enable Smart Trash? This creates an alias for "rm" that moves files to ~/.claude-mem/trash instead of permanently deleting them. You can restore files anytime by typing "claude-mem restore".',
|
||||
initialValue: true
|
||||
});
|
||||
|
||||
if (p.isCancel(enableSmartTrash)) {
|
||||
p.cancel('Installation cancelled');
|
||||
return null;
|
||||
}
|
||||
|
||||
config.enableSmartTrash = enableSmartTrash;
|
||||
|
||||
// Ask about save-on-clear
|
||||
const saveMemoriesOnClear = await p.confirm({
|
||||
message: 'Would you like to save memories when you type "/clear" in Claude Code? When running /clear with this on, it takes about a minute to save memories before your new session starts.',
|
||||
initialValue: false
|
||||
});
|
||||
|
||||
if (p.isCancel(saveMemoriesOnClear)) {
|
||||
p.cancel('Installation cancelled');
|
||||
return null;
|
||||
}
|
||||
|
||||
config.saveMemoriesOnClear = saveMemoriesOnClear;
|
||||
|
||||
return config as InstallConfig;
|
||||
}
|
||||
// </Block>
|
||||
|
||||
|
||||
// <Block> Directory structure creation - natural setup flow
|
||||
function ensureDirectoryStructure(): void {
|
||||
const pathDiscovery = PathDiscovery.getInstance();
|
||||
|
||||
// Create all data directories
|
||||
pathDiscovery.ensureAllDataDirectories();
|
||||
|
||||
// Create all Claude integration directories
|
||||
pathDiscovery.ensureAllClaudeDirectories();
|
||||
|
||||
// Create package.json in .claude-mem to fix ESM module issues
|
||||
const packageJsonPath = join(pathDiscovery.getDataDirectory(), 'package.json');
|
||||
if (!existsSync(packageJsonPath)) {
|
||||
const packageJson = {
|
||||
name: "claude-mem-data",
|
||||
type: "module"
|
||||
};
|
||||
writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
|
||||
}
|
||||
}
|
||||
// </Block>
|
||||
|
||||
function copyFileRecursively(src: string, dest: string): void {
|
||||
const stat = statSync(src);
|
||||
if (stat.isDirectory()) {
|
||||
if (!existsSync(dest)) {
|
||||
mkdirSync(dest, { recursive: true });
|
||||
}
|
||||
const files = readdirSync(src);
|
||||
files.forEach((file: string) => {
|
||||
copyFileRecursively(join(src, file), join(dest, file));
|
||||
});
|
||||
} else {
|
||||
copyFileSync(src, dest);
|
||||
}
|
||||
}
|
||||
|
||||
function writeHookFiles(timeout: number = 180000): void {
|
||||
const pathDiscovery = PathDiscovery.getInstance();
|
||||
const runtimeHooksDir = pathDiscovery.getHooksDirectory();
|
||||
const packageHookTemplatesDir = pathDiscovery.findPackageHookTemplatesDirectory();
|
||||
|
||||
const hookFiles = ['session-start.js', 'stop.js', 'user-prompt-submit.js', 'post-tool-use.js'];
|
||||
|
||||
for (const hookFile of hookFiles) {
|
||||
const sourceTemplatePath = join(packageHookTemplatesDir, hookFile);
|
||||
const runtimeHookPath = join(runtimeHooksDir, hookFile);
|
||||
copyFileSync(sourceTemplatePath, runtimeHookPath);
|
||||
Platform.makeExecutable(runtimeHookPath);
|
||||
}
|
||||
|
||||
const sourceSharedTemplateDir = join(packageHookTemplatesDir, 'shared');
|
||||
const runtimeSharedDir = join(runtimeHooksDir, 'shared');
|
||||
if (existsSync(sourceSharedTemplateDir)) {
|
||||
copyFileRecursively(sourceSharedTemplateDir, runtimeSharedDir);
|
||||
}
|
||||
|
||||
const hookConfig = {
|
||||
packageName: PACKAGE_NAME,
|
||||
cliCommand: PACKAGE_NAME,
|
||||
backend: 'chroma',
|
||||
timeout
|
||||
};
|
||||
writeFileSync(join(runtimeHooksDir, 'config.json'), JSON.stringify(hookConfig, null, 2));
|
||||
|
||||
// Create package.json and install dependencies in hooks directory
|
||||
const hookPackageJson = {
|
||||
name: "claude-mem-hooks",
|
||||
type: "module",
|
||||
dependencies: {
|
||||
"better-sqlite3": "^11.8.0"
|
||||
}
|
||||
};
|
||||
writeFileSync(join(runtimeHooksDir, 'package.json'), JSON.stringify(hookPackageJson, null, 2));
|
||||
|
||||
// Install dependencies
|
||||
try {
|
||||
execSync('npm install --silent', {
|
||||
cwd: runtimeHooksDir,
|
||||
stdio: 'pipe'
|
||||
});
|
||||
} catch (error) {
|
||||
// Silent fail - hooks might still work if better-sqlite3 is globally available
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function ensureClaudeMdInstructions(): void {
|
||||
const pathDiscovery = PathDiscovery.getInstance();
|
||||
const claudeMdPath = pathDiscovery.getClaudeMdPath();
|
||||
const claudeMdDir = dirname(claudeMdPath);
|
||||
|
||||
// Ensure .claude directory exists
|
||||
if (!existsSync(claudeMdDir)) {
|
||||
mkdirSync(claudeMdDir, { recursive: true });
|
||||
}
|
||||
|
||||
const instructions = `
|
||||
<!-- CLAUDE-MEM QUICK REFERENCE -->
|
||||
## 🧠 Memory System Quick Reference
|
||||
|
||||
### Search Your Memories (SIMPLE & POWERFUL)
|
||||
- **Semantic search is king**: \`mcp__claude-mem__chroma_query_documents(["search terms"])\`
|
||||
- **🔒 ALWAYS include project name in query**: \`["claude-mem feature authentication"]\` not just \`["feature authentication"]\`
|
||||
- **Include dates for temporal search**: \`["project-name 2025-09-09 bug fix"]\` finds memories from that date
|
||||
- **Get specific memory**: \`mcp__claude-mem__chroma_get_documents(ids: ["document_id"])\`
|
||||
|
||||
### Search Tips That Actually Work
|
||||
- **Project isolation**: Always prefix queries with project name to avoid cross-contamination
|
||||
- **Temporal search**: Include dates (YYYY-MM-DD) in query text to find memories from specific times
|
||||
- **Intent-based**: "implementing oauth" > "oauth implementation code function"
|
||||
- **Multiple queries**: Search with different phrasings for better coverage
|
||||
- **Session-specific**: Include session ID in query when you know it
|
||||
|
||||
### What Doesn't Work (Don't Do This!)
|
||||
- ❌ Complex where filters with $and/$or - they cause errors
|
||||
- ❌ Timestamp comparisons ($gte/$lt) - Chroma stores timestamps as strings
|
||||
- ❌ Mixing project filters in where clause - causes "Error finding id"
|
||||
|
||||
### Storage
|
||||
- Collection: "claude_memories"
|
||||
- Archives: ~/.claude-mem/archives/
|
||||
<!-- /CLAUDE-MEM QUICK REFERENCE -->`;
|
||||
|
||||
// Check if file exists and read content
|
||||
let content = '';
|
||||
if (existsSync(claudeMdPath)) {
|
||||
content = readFileSync(claudeMdPath, 'utf8');
|
||||
|
||||
// Check if instructions already exist (handle both old and new format)
|
||||
const hasOldInstructions = content.includes('<!-- CLAUDE-MEM INSTRUCTIONS -->');
|
||||
const hasNewInstructions = content.includes('<!-- CLAUDE-MEM QUICK REFERENCE -->');
|
||||
|
||||
if (hasOldInstructions || hasNewInstructions) {
|
||||
// Replace existing instructions (handle both old and new markers)
|
||||
let startMarker, endMarker;
|
||||
if (hasOldInstructions) {
|
||||
startMarker = '<!-- CLAUDE-MEM INSTRUCTIONS -->';
|
||||
endMarker = '<!-- /CLAUDE-MEM INSTRUCTIONS -->';
|
||||
} else {
|
||||
startMarker = '<!-- CLAUDE-MEM QUICK REFERENCE -->';
|
||||
endMarker = '<!-- /CLAUDE-MEM QUICK REFERENCE -->';
|
||||
}
|
||||
|
||||
const startIndex = content.indexOf(startMarker);
|
||||
const endIndex = content.indexOf(endMarker) + endMarker.length;
|
||||
|
||||
if (startIndex !== -1 && endIndex !== -1) {
|
||||
content = content.substring(0, startIndex) + instructions.trim() + content.substring(endIndex);
|
||||
}
|
||||
} else {
|
||||
// Append instructions to the end
|
||||
content = content.trim() + '\n' + instructions;
|
||||
}
|
||||
} else {
|
||||
// Create new file with instructions
|
||||
content = instructions.trim();
|
||||
}
|
||||
|
||||
// Write the updated content
|
||||
writeFileSync(claudeMdPath, content);
|
||||
}
|
||||
|
||||
function installChromaMcp(forceReinstall: boolean = false): void {
|
||||
const uvPath = `${homedir()}/.cargo/bin`;
|
||||
if (existsSync(uvPath) && !process.env.PATH?.includes(uvPath)) {
|
||||
process.env.PATH = `${uvPath}:${process.env.PATH}`;
|
||||
}
|
||||
|
||||
if (forceReinstall) {
|
||||
try {
|
||||
execSync('claude mcp remove claude-mem', { stdio: 'pipe' });
|
||||
} catch (error) {
|
||||
// Ignore errors if claude-mem doesn't exist
|
||||
}
|
||||
}
|
||||
|
||||
const chromaMcpCommand = `claude mcp add claude-mem -- uvx chroma-mcp --client-type persistent --data-dir ${PathDiscovery.getInstance().getChromaDirectory()}`;
|
||||
execSync(chromaMcpCommand, { stdio: 'inherit' });
|
||||
}
|
||||
|
||||
function createHookConfig(scriptPath: string, timeout: number, matcher?: string) {
|
||||
const config: any = {
|
||||
hooks: [{ type: "command", command: scriptPath, timeout }]
|
||||
};
|
||||
if (matcher) config.matcher = matcher;
|
||||
return config;
|
||||
}
|
||||
|
||||
function configureHooks(settingsPath: string): void {
|
||||
const pathDiscovery = PathDiscovery.getInstance();
|
||||
const hooksDir = pathDiscovery.getHooksDirectory();
|
||||
|
||||
let settings: any = existsSync(settingsPath)
|
||||
? JSON.parse(readFileSync(settingsPath, 'utf8'))
|
||||
: { hooks: {} };
|
||||
|
||||
mkdirSync(dirname(settingsPath), { recursive: true });
|
||||
|
||||
if (!settings.hooks) settings.hooks = {};
|
||||
|
||||
const hookTypes = ['SessionStart', 'Stop', 'UserPromptSubmit', 'PostToolUse'];
|
||||
hookTypes.forEach(type => {
|
||||
if (settings.hooks[type]) {
|
||||
settings.hooks[type] = settings.hooks[type].filter(
|
||||
(cfg: any) => !cfg.hooks?.some((h: any) => h.command?.includes(PACKAGE_NAME))
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
settings.hooks.SessionStart = [createHookConfig(join(hooksDir, 'session-start.js'), 180)];
|
||||
settings.hooks.Stop = [createHookConfig(join(hooksDir, 'stop.js'), 60)];
|
||||
settings.hooks.UserPromptSubmit = [createHookConfig(join(hooksDir, 'user-prompt-submit.js'), 60)];
|
||||
settings.hooks.PostToolUse = [createHookConfig(join(hooksDir, 'post-tool-use.js'), 180, "*")];
|
||||
|
||||
writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
||||
}
|
||||
|
||||
function getSettingsPath(config: InstallConfig): string {
|
||||
if (config.scope === 'local' && config.customPath) {
|
||||
return join(config.customPath, 'settings.local.json');
|
||||
} else if (config.scope === 'project') {
|
||||
return join(process.cwd(), '.claude', 'settings.json');
|
||||
} else {
|
||||
return PathDiscovery.getInstance().getClaudeSettingsPath();
|
||||
}
|
||||
}
|
||||
|
||||
function configureUserSettings(config: InstallConfig): void {
|
||||
const pathDiscovery = PathDiscovery.getInstance();
|
||||
const userSettingsPath = pathDiscovery.getUserSettingsPath();
|
||||
|
||||
let userSettings: Settings = existsSync(userSettingsPath)
|
||||
? JSON.parse(readFileSync(userSettingsPath, 'utf8'))
|
||||
: {};
|
||||
|
||||
userSettings.backend = 'chroma';
|
||||
userSettings.installed = true;
|
||||
userSettings.embedded = true;
|
||||
userSettings.saveMemoriesOnClear = config.saveMemoriesOnClear || false;
|
||||
userSettings.claudePath = detectClaudePath();
|
||||
|
||||
writeFileSync(userSettingsPath, JSON.stringify(userSettings, null, 2));
|
||||
}
|
||||
|
||||
function configureSmartTrashAlias(): void {
|
||||
const shellConfigs = Platform.getShellConfigPaths();
|
||||
const aliasDefinition = Platform.getAliasDefinition('rm', 'claude-mem trash');
|
||||
const commentLine = Platform.isWindows()
|
||||
? '# claude-mem smart trash alias'
|
||||
: '# claude-mem smart trash alias';
|
||||
|
||||
for (const configPath of shellConfigs) {
|
||||
if (!existsSync(configPath)) {
|
||||
// Create the file if it doesn't exist (especially for PowerShell profiles)
|
||||
const dir = dirname(configPath);
|
||||
if (!existsSync(dir)) {
|
||||
mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
writeFileSync(configPath, '');
|
||||
}
|
||||
|
||||
let content = readFileSync(configPath, 'utf8');
|
||||
if (content.includes(aliasDefinition)) continue;
|
||||
|
||||
const aliasBlock = `\n${commentLine}\n${aliasDefinition}\n`;
|
||||
content += aliasBlock;
|
||||
writeFileSync(configPath, content);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function installClaudeCommands(): void {
|
||||
const pathDiscovery = PathDiscovery.getInstance();
|
||||
const claudeCommandsDir = pathDiscovery.getClaudeCommandsDirectory();
|
||||
const packageCommandsDir = pathDiscovery.findPackageCommandsDirectory();
|
||||
|
||||
mkdirSync(claudeCommandsDir, { recursive: true });
|
||||
|
||||
const commandFiles = ['save.md', 'remember.md', 'claude-mem.md'];
|
||||
|
||||
for (const fileName of commandFiles) {
|
||||
const sourcePath = join(packageCommandsDir, fileName);
|
||||
const destPath = join(claudeCommandsDir, fileName);
|
||||
if (existsSync(sourcePath)) {
|
||||
copyFileSync(sourcePath, destPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export async function install(options: OptionValues = {}): Promise<void> {
|
||||
console.log(fastRainbow('\n═══════════════════════════════════════'));
|
||||
console.log(fastRainbow(' CLAUDE-MEM INSTALLER '));
|
||||
console.log(fastRainbow('═══════════════════════════════════════'));
|
||||
|
||||
console.log(boxen(vibrantRainbow('🧠 Persistent Memory System for Claude Code\n\n✨ Transform your Claude experience with seamless context preservation\n🚀 Never lose your conversation history again'), {
|
||||
padding: 2,
|
||||
margin: 1,
|
||||
borderStyle: 'double',
|
||||
borderColor: 'magenta',
|
||||
textAlignment: 'center'
|
||||
}));
|
||||
|
||||
await sleep(500);
|
||||
|
||||
installUv();
|
||||
|
||||
const isNonInteractive = options.user || options.project || options.local || options.force;
|
||||
|
||||
let config: InstallConfig;
|
||||
|
||||
if (isNonInteractive) {
|
||||
config = {
|
||||
scope: options.local ? 'local' : options.project ? 'project' : 'user',
|
||||
customPath: options.path,
|
||||
hookTimeout: options.timeout ? parseInt(options.timeout) : 180,
|
||||
forceReinstall: !!options.force,
|
||||
enableSmartTrash: false,
|
||||
saveMemoriesOnClear: false
|
||||
};
|
||||
} else {
|
||||
const existingInstall = hasExistingInstallation();
|
||||
const wizardConfig = await runInstallationWizard(existingInstall);
|
||||
if (!wizardConfig) {
|
||||
process.exit(0);
|
||||
}
|
||||
config = wizardConfig;
|
||||
}
|
||||
|
||||
console.log(vibrantRainbow('\n🚀 Beginning Installation Process\n'));
|
||||
|
||||
const steps = [
|
||||
{ name: 'Creating directory structure', fn: () => ensureDirectoryStructure() },
|
||||
{ name: 'Installing Chroma MCP server', fn: () => installChromaMcp(config.forceReinstall) },
|
||||
{ name: 'Adding CLAUDE.md instructions', fn: () => ensureClaudeMdInstructions() },
|
||||
{ name: 'Installing Claude commands', fn: () => installClaudeCommands() },
|
||||
{ name: 'Installing memory hooks', fn: () => writeHookFiles(config.hookTimeout) },
|
||||
{ name: 'Configuring Claude settings', fn: () => configureHooks(getSettingsPath(config)) },
|
||||
{ name: 'Configuring user settings', fn: () => configureUserSettings(config) }
|
||||
];
|
||||
|
||||
if (config.enableSmartTrash) {
|
||||
steps.push({ name: 'Configuring Smart Trash alias', fn: () => configureSmartTrashAlias() });
|
||||
}
|
||||
|
||||
for (let i = 0; i < steps.length; i++) {
|
||||
const step = steps[i];
|
||||
const progress = `[${i + 1}/${steps.length}]`;
|
||||
|
||||
const loader = createLoadingAnimation(`${chalk.gray(progress)} ${step.name}...`);
|
||||
loader.start();
|
||||
|
||||
step.fn();
|
||||
loader.stop(`${chalk.gray(progress)} ${step.name} ${vibrantRainbow('completed! ✨')}`);
|
||||
|
||||
await sleep(150);
|
||||
}
|
||||
|
||||
|
||||
// Beautiful success message
|
||||
const successTitle = fastRainbow('🎉 INSTALLATION COMPLETE! 🎉');
|
||||
|
||||
const successMessage = `
|
||||
${chalk.bold('How your new memory system works:')}
|
||||
|
||||
${chalk.green('•')} When you start Claude Code, claude-mem loads your latest memories automatically
|
||||
${chalk.green('•')} Memories are saved automatically as you work
|
||||
${chalk.green('•')} Ask Claude to search your memories anytime with natural language
|
||||
${chalk.green('•')} Instructions added to ${chalk.cyan('~/.claude/CLAUDE.md')} teach Claude how to use the system
|
||||
|
||||
${chalk.bold('Slash Commands Available:')}
|
||||
${chalk.cyan('/claude-mem help')} - Show all memory commands and features
|
||||
${chalk.cyan('/save')} - Quick save of current conversation overview
|
||||
${chalk.cyan('/remember')} - Search your saved memories
|
||||
|
||||
${chalk.bold('Quick Start:')}
|
||||
${chalk.yellow('1.')} Restart Claude Code to activate your memory system
|
||||
${chalk.yellow('2.')} Start using Claude normally - memories save automatically
|
||||
${chalk.yellow('3.')} Search memories by asking: ${chalk.italic('"Search my memories for X"')}`;
|
||||
|
||||
|
||||
const finalSmartTrashNote = config.enableSmartTrash ?
|
||||
`\n\n${chalk.blue('🗑️ Smart Trash Enabled:')}
|
||||
${chalk.gray(' • rm commands now move files to ~/.claude-mem/trash')}
|
||||
${chalk.gray(' • View trash:')} ${chalk.cyan('claude-mem trash view')}
|
||||
${chalk.gray(' • Restore files:')} ${chalk.cyan('claude-mem restore')}
|
||||
${chalk.gray(' • Empty trash:')} ${chalk.cyan('claude-mem trash empty')}
|
||||
${chalk.yellow(' • Restart terminal for alias to activate')}` : '';
|
||||
|
||||
const finalClearHookNote = config.saveMemoriesOnClear ?
|
||||
`\n\n${chalk.magenta('💾 Save-on-clear enabled:')}
|
||||
${chalk.gray(' • /clear now saves memories automatically (takes ~1 minute)')}` : '';
|
||||
|
||||
console.log(boxen(successTitle + successMessage + finalSmartTrashNote + finalClearHookNote, {
|
||||
padding: 2,
|
||||
margin: 1,
|
||||
borderStyle: 'double',
|
||||
borderColor: 'green',
|
||||
backgroundColor: '#001122'
|
||||
}));
|
||||
|
||||
// Final flourish
|
||||
console.log(fastRainbow('\n✨ Welcome to the future of persistent AI conversations! ✨\n'));
|
||||
}
|
||||
@@ -0,0 +1,461 @@
|
||||
import { OptionValues } from 'commander';
|
||||
import fs from 'fs';
|
||||
import { join } from 'path';
|
||||
import { PathDiscovery } from '../services/path-discovery.js';
|
||||
import {
|
||||
createCompletionMessage,
|
||||
createContextualError,
|
||||
createUserFriendlyError,
|
||||
formatTimeAgo,
|
||||
outputSessionStartContent
|
||||
} from '../prompts/templates/context/ContextTemplates.js';
|
||||
import { getStorageProvider, needsMigration } from '../shared/storage.js';
|
||||
import { MemoryRow, OverviewRow } from '../services/sqlite/types.js';
|
||||
import { createStores } from '../services/sqlite/index.js';
|
||||
import { getRollingSettings } from '../shared/rolling-settings.js';
|
||||
import { rollingLog } from '../shared/rolling-log.js';
|
||||
|
||||
interface TrashStatus {
|
||||
folderCount: number;
|
||||
fileCount: number;
|
||||
totalSize: number;
|
||||
isEmpty: boolean;
|
||||
}
|
||||
|
||||
function formatDateHeader(date = new Date()): string {
|
||||
return date.toLocaleString('en-US', {
|
||||
weekday: 'long',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
timeZoneName: 'short'
|
||||
});
|
||||
}
|
||||
|
||||
function wordWrap(text: string, maxWidth: number, prefix: string): string {
|
||||
const words = text.split(' ');
|
||||
const lines: string[] = [];
|
||||
let currentLine = prefix;
|
||||
const continuationPrefix = ' '.repeat(prefix.length);
|
||||
|
||||
for (const word of words) {
|
||||
const needsSpace = currentLine !== prefix && currentLine !== continuationPrefix;
|
||||
const testLine = currentLine + (needsSpace ? ' ' : '') + word;
|
||||
|
||||
if (testLine.length <= maxWidth) {
|
||||
currentLine = testLine;
|
||||
} else {
|
||||
if (currentLine.trim()) {
|
||||
lines.push(currentLine);
|
||||
}
|
||||
currentLine = continuationPrefix + word;
|
||||
}
|
||||
}
|
||||
|
||||
if (currentLine.trim()) {
|
||||
lines.push(currentLine);
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
function buildProjectMatcher(projectName: string): (value?: string) => boolean {
|
||||
const aliases = new Set<string>();
|
||||
aliases.add(projectName);
|
||||
aliases.add(projectName.replace(/-/g, '_'));
|
||||
aliases.add(projectName.replace(/_/g, '-'));
|
||||
return (value?: string) => !!value && aliases.has(value);
|
||||
}
|
||||
|
||||
function formatSize(bytes: number): string {
|
||||
if (bytes === 0) return '0 B';
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
function getTrashStatus(): TrashStatus {
|
||||
const trashDir = PathDiscovery.getInstance().getTrashDirectory();
|
||||
|
||||
if (!fs.existsSync(trashDir)) {
|
||||
return { folderCount: 0, fileCount: 0, totalSize: 0, isEmpty: true };
|
||||
}
|
||||
|
||||
const items = fs.readdirSync(trashDir);
|
||||
if (items.length === 0) {
|
||||
return { folderCount: 0, fileCount: 0, totalSize: 0, isEmpty: true };
|
||||
}
|
||||
|
||||
let folderCount = 0;
|
||||
let fileCount = 0;
|
||||
let totalSize = 0;
|
||||
|
||||
for (const item of items) {
|
||||
const itemPath = join(trashDir, item);
|
||||
const stats = fs.statSync(itemPath);
|
||||
|
||||
if (stats.isDirectory()) {
|
||||
folderCount++;
|
||||
} else {
|
||||
fileCount++;
|
||||
}
|
||||
|
||||
totalSize += stats.size;
|
||||
}
|
||||
|
||||
return { folderCount, fileCount, totalSize, isEmpty: false };
|
||||
}
|
||||
|
||||
async function renderRollingSessionStart(projectOverride?: string): Promise<void> {
|
||||
const settings = getRollingSettings();
|
||||
|
||||
if (!settings.sessionStartEnabled) {
|
||||
console.log('Rolling session-start output disabled in settings.');
|
||||
rollingLog('info', 'session-start output skipped (disabled)', {
|
||||
project: projectOverride
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const stores = await createStores();
|
||||
const projectName = projectOverride || PathDiscovery.getCurrentProjectName();
|
||||
|
||||
// Get all overviews for this project (oldest to newest)
|
||||
const allOverviews = stores.overviews.getAllForProject(projectName);
|
||||
|
||||
// Limit to last 10 overviews
|
||||
const recentOverviews = allOverviews.slice(-10);
|
||||
|
||||
// If no data at all, show friendly message
|
||||
if (recentOverviews.length === 0) {
|
||||
console.log('===============================================================================');
|
||||
console.log(`What's new | ${formatDateHeader()}`);
|
||||
console.log('===============================================================================');
|
||||
console.log('No previous sessions found for this project.');
|
||||
console.log('Start working and claude-mem will automatically capture context for future sessions.');
|
||||
console.log('===============================================================================');
|
||||
const trashStatus = getTrashStatus();
|
||||
if (!trashStatus.isEmpty) {
|
||||
const formattedSize = formatSize(trashStatus.totalSize);
|
||||
console.log(
|
||||
`🗑️ Trash – ${trashStatus.folderCount} folders | ${trashStatus.fileCount} files | ${formattedSize} – use \`claude-mem restore\``
|
||||
);
|
||||
console.log('===============================================================================');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Output header
|
||||
console.log('===============================================================================');
|
||||
console.log(`What's new | ${formatDateHeader()}`);
|
||||
console.log('===============================================================================');
|
||||
|
||||
// Output each overview with timestamp, memory names, and files touched (oldest to newest)
|
||||
recentOverviews.forEach((overview) => {
|
||||
const date = new Date(overview.created_at);
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
const hours = date.getHours();
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||
const ampm = hours >= 12 ? 'PM' : 'AM';
|
||||
const displayHours = hours % 12 || 12;
|
||||
|
||||
console.log(`[${year}-${month}-${day} at ${displayHours}:${minutes} ${ampm}]`);
|
||||
|
||||
// Get memories for this session to show titles, subtitles, files, and keywords
|
||||
const sessionMemories = stores.memories.getBySessionId(overview.session_id);
|
||||
|
||||
// Extract memory titles and subtitles
|
||||
const memories = sessionMemories
|
||||
.map(m => ({ title: m.title, subtitle: m.subtitle }))
|
||||
.filter(m => m.title);
|
||||
|
||||
// Extract unique files touched across all memories
|
||||
const allFilesTouched = new Set<string>();
|
||||
const allKeywords = new Set<string>();
|
||||
|
||||
sessionMemories.forEach(m => {
|
||||
if (m.files_touched) {
|
||||
try {
|
||||
const files = JSON.parse(m.files_touched);
|
||||
if (Array.isArray(files)) {
|
||||
files.forEach(f => allFilesTouched.add(f));
|
||||
}
|
||||
} catch (e) {
|
||||
// Skip invalid JSON
|
||||
}
|
||||
}
|
||||
|
||||
if (m.keywords) {
|
||||
// Keywords are comma-separated
|
||||
m.keywords.split(',').forEach(k => allKeywords.add(k.trim()));
|
||||
}
|
||||
});
|
||||
|
||||
console.log('');
|
||||
|
||||
// Always show overview content
|
||||
console.log(wordWrap(overview.content, 80, ''));
|
||||
|
||||
// Display files touched if any
|
||||
if (allFilesTouched.size > 0) {
|
||||
console.log('');
|
||||
console.log(wordWrap(`- ${Array.from(allFilesTouched).join(', ')}`, 80, ''));
|
||||
}
|
||||
|
||||
// Display keywords/tags if any
|
||||
if (allKeywords.size > 0) {
|
||||
console.log('');
|
||||
console.log(wordWrap(`Tags: ${Array.from(allKeywords).join(', ')}`, 80, ''));
|
||||
}
|
||||
|
||||
console.log('');
|
||||
});
|
||||
|
||||
console.log('===============================================================================');
|
||||
const trashStatus = getTrashStatus();
|
||||
if (!trashStatus.isEmpty) {
|
||||
const formattedSize = formatSize(trashStatus.totalSize);
|
||||
console.log(
|
||||
`🗑️ Trash – ${trashStatus.folderCount} folders | ${trashStatus.fileCount} files | ${formattedSize} – use \`claude-mem restore\``
|
||||
);
|
||||
console.log('===============================================================================');
|
||||
}
|
||||
}
|
||||
|
||||
export async function loadContext(options: OptionValues = {}): Promise<void> {
|
||||
try {
|
||||
// Check if migration is needed and warn the user
|
||||
if (await needsMigration()) {
|
||||
console.warn('⚠️ JSONL to SQLite migration recommended. Run: claude-mem migrate-index');
|
||||
}
|
||||
|
||||
const storage = await getStorageProvider();
|
||||
|
||||
// If using JSONL fallback, use original implementation
|
||||
if (storage.backend === 'jsonl') {
|
||||
return await loadContextFromJSONL(options);
|
||||
}
|
||||
|
||||
// SQLite implementation - fetch data using storage provider
|
||||
let recentMemories: MemoryRow[] = [];
|
||||
let recentOverviews: OverviewRow[] = [];
|
||||
|
||||
// Auto-detect current project for session-start format if no project specified
|
||||
let projectToUse = options.project;
|
||||
if (!projectToUse && options.format === 'session-start') {
|
||||
projectToUse = PathDiscovery.getCurrentProjectName();
|
||||
}
|
||||
|
||||
if (options.format === 'session-start') {
|
||||
await renderRollingSessionStart(projectToUse);
|
||||
return;
|
||||
}
|
||||
|
||||
const overviewLimit = options.format === 'json' ? 5 : 3;
|
||||
|
||||
if (projectToUse) {
|
||||
recentMemories = await storage.getRecentMemoriesForProject(projectToUse, 10);
|
||||
recentOverviews = await storage.getRecentOverviewsForProject(projectToUse, overviewLimit);
|
||||
} else {
|
||||
recentMemories = await storage.getRecentMemories(10);
|
||||
recentOverviews = await storage.getRecentOverviews(overviewLimit);
|
||||
}
|
||||
|
||||
// Convert SQLite rows to JSONL format for compatibility with existing output functions
|
||||
const memoriesAsJSON = recentMemories.map(row => ({
|
||||
type: 'memory',
|
||||
text: row.text,
|
||||
document_id: row.document_id,
|
||||
keywords: row.keywords,
|
||||
session_id: row.session_id,
|
||||
project: row.project,
|
||||
timestamp: row.created_at,
|
||||
archive: row.archive_basename
|
||||
}));
|
||||
|
||||
const overviewsAsJSON = recentOverviews.map(row => ({
|
||||
type: 'overview',
|
||||
content: row.content,
|
||||
session_id: row.session_id,
|
||||
project: row.project,
|
||||
timestamp: row.created_at
|
||||
}));
|
||||
|
||||
// If no data found, show appropriate messages
|
||||
if (memoriesAsJSON.length === 0 && overviewsAsJSON.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.format === 'json') {
|
||||
// For JSON format, combine last 10 of each type
|
||||
const recentObjects = [...memoriesAsJSON, ...overviewsAsJSON];
|
||||
console.log(JSON.stringify(recentObjects));
|
||||
} else {
|
||||
// Default format - show last 10 memories and last 3 overviews
|
||||
const totalCount = memoriesAsJSON.length + overviewsAsJSON.length;
|
||||
|
||||
console.log(createCompletionMessage('Context loading', totalCount, 'recent entries found'));
|
||||
|
||||
// Show memories first
|
||||
memoriesAsJSON.forEach((obj) => {
|
||||
console.log(`${obj.text} | ${obj.document_id} | ${obj.keywords}`);
|
||||
});
|
||||
|
||||
// Then show overviews
|
||||
overviewsAsJSON.forEach((obj) => {
|
||||
console.log(`**Overview:** ${obj.content}`);
|
||||
});
|
||||
}
|
||||
|
||||
// Display trash status if not empty (except for JSON format to avoid breaking JSON parsing)
|
||||
if (options.format !== 'json') {
|
||||
const trashStatus = getTrashStatus();
|
||||
if (!trashStatus.isEmpty) {
|
||||
const formattedSize = formatSize(trashStatus.totalSize);
|
||||
console.log(`🗑️ Trash – ${trashStatus.folderCount} folders | ${trashStatus.fileCount} files | ${formattedSize} – use \`claude-mem restore\``);
|
||||
console.log('');
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
if (options.format === 'session-start') {
|
||||
console.log(createContextualError('CONNECTION_FAILED', errorMessage));
|
||||
} else {
|
||||
console.log(createUserFriendlyError('Context loading', errorMessage, 'Check file permissions and try again'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Original JSONL-based implementation for fallback compatibility
|
||||
*/
|
||||
async function loadContextFromJSONL(options: OptionValues = {}): Promise<void> {
|
||||
const pathDiscovery = PathDiscovery.getInstance();
|
||||
const indexPath = pathDiscovery.getIndexPath();
|
||||
|
||||
// Auto-detect current project for session-start format if no project specified
|
||||
let projectToUse = options.project;
|
||||
if (!projectToUse && options.format === 'session-start') {
|
||||
projectToUse = PathDiscovery.getCurrentProjectName();
|
||||
}
|
||||
|
||||
// Check if index file exists
|
||||
if (!fs.existsSync(indexPath)) {
|
||||
if (options.format === 'session-start') {
|
||||
console.log(createContextualError('NO_MEMORIES', projectToUse || 'this project'));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(indexPath, 'utf-8');
|
||||
const lines = content.trim().split('\n').filter(line => line.trim());
|
||||
|
||||
if (lines.length === 0) {
|
||||
if (options.format === 'session-start') {
|
||||
console.log(createContextualError('NO_MEMORIES', projectToUse || 'this project'));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse JSONL format - each line is a JSON object
|
||||
const jsonObjects: any[] = [];
|
||||
for (const line of lines) {
|
||||
try {
|
||||
// Skip lines that don't look like JSON (could be legacy format)
|
||||
if (!line.trim().startsWith('{')) {
|
||||
continue;
|
||||
}
|
||||
const obj = JSON.parse(line);
|
||||
jsonObjects.push(obj);
|
||||
} catch (e) {
|
||||
// Skip malformed JSON lines
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (jsonObjects.length === 0) {
|
||||
if (options.format === 'session-start') {
|
||||
console.log(createContextualError('NO_MEMORIES', projectToUse || 'this project'));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Separate memories, overviews, and other types
|
||||
const memories = jsonObjects.filter(obj => obj.type === 'memory');
|
||||
const overviews = jsonObjects.filter(obj => obj.type === 'overview');
|
||||
const sessions = jsonObjects.filter(obj => obj.type === 'session');
|
||||
|
||||
// Filter each type by project if specified
|
||||
// Handle both hyphen and underscore formats since index has mixed entries
|
||||
let filteredMemories = memories;
|
||||
let filteredOverviews = overviews;
|
||||
let filteredSessions = sessions;
|
||||
if (projectToUse) {
|
||||
const matchesProject = buildProjectMatcher(projectToUse);
|
||||
filteredMemories = memories.filter(obj => matchesProject(obj.project));
|
||||
filteredOverviews = overviews.filter(obj => matchesProject(obj.project));
|
||||
filteredSessions = sessions.filter(obj => matchesProject(obj.project));
|
||||
}
|
||||
|
||||
if (options.format === 'session-start') {
|
||||
// Get last 10 memories and last 10 overviews for session-start
|
||||
const recentMemories = filteredMemories.slice(-10);
|
||||
const recentOverviews = filteredOverviews.slice(-10);
|
||||
const recentSessions = filteredSessions.slice(-10);
|
||||
|
||||
// Combine them for the display
|
||||
const recentObjects = [...recentSessions, ...recentMemories, ...recentOverviews];
|
||||
|
||||
// Find most recent timestamp for last session info
|
||||
let lastSessionTime = 'recently';
|
||||
const timestamps = recentObjects
|
||||
.map(obj => {
|
||||
// Get timestamp from JSON object
|
||||
return obj.timestamp ? new Date(obj.timestamp) : null;
|
||||
})
|
||||
.filter(date => date !== null)
|
||||
.sort((a, b) => b.getTime() - a.getTime());
|
||||
|
||||
if (timestamps.length > 0) {
|
||||
lastSessionTime = formatTimeAgo(timestamps[0]);
|
||||
}
|
||||
|
||||
// Use dual-stream output for session start formatting
|
||||
outputSessionStartContent({
|
||||
projectName: projectToUse || 'your project',
|
||||
memoryCount: recentMemories.length,
|
||||
lastSessionTime,
|
||||
recentObjects
|
||||
});
|
||||
|
||||
} else if (options.format === 'json') {
|
||||
// For JSON format, combine last 10 of each type
|
||||
const recentMemories = filteredMemories.slice(-10);
|
||||
const recentOverviews = filteredOverviews.slice(-3);
|
||||
const recentObjects = [...recentMemories, ...recentOverviews];
|
||||
console.log(JSON.stringify(recentObjects));
|
||||
} else {
|
||||
// Default format - show last 10 memories and last 3 overviews
|
||||
const recentMemories = filteredMemories.slice(-10);
|
||||
const recentOverviews = filteredOverviews.slice(-3);
|
||||
const totalCount = recentMemories.length + recentOverviews.length;
|
||||
|
||||
console.log(createCompletionMessage('Context loading', totalCount, 'recent entries found'));
|
||||
|
||||
// Show memories first
|
||||
recentMemories.forEach((obj) => {
|
||||
console.log(`${obj.text} | ${obj.document_id} | ${obj.keywords}`);
|
||||
});
|
||||
|
||||
// Then show overviews
|
||||
recentOverviews.forEach((obj) => {
|
||||
console.log(`**Overview:** ${obj.content}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import { OptionValues } from 'commander';
|
||||
import { readFileSync, readdirSync, statSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { PathDiscovery } from '../services/path-discovery.js';
|
||||
|
||||
// <Block> 1.1 ====================================
|
||||
async function showLog(logPath: string, logType: string, tail: number): Promise<void> {
|
||||
// <Block> 1.2 ====================================
|
||||
try {
|
||||
const content = readFileSync(logPath, 'utf8');
|
||||
const lines = content.split('\n').filter(line => line.trim());
|
||||
const displayLines = lines.slice(-tail);
|
||||
|
||||
console.log(`📋 ${logType} Logs (last ${tail} lines):`);
|
||||
console.log(` File: ${logPath}`);
|
||||
console.log('');
|
||||
|
||||
// <Block> 1.3 ====================================
|
||||
if (displayLines.length === 0) {
|
||||
console.log(' No log entries found');
|
||||
// </Block> =======================================
|
||||
} else {
|
||||
displayLines.forEach(line => {
|
||||
console.log(` ${line}`);
|
||||
});
|
||||
}
|
||||
// </Block> =======================================
|
||||
|
||||
console.log('');
|
||||
// </Block> =======================================
|
||||
} catch (error) {
|
||||
// <Block> 1.4 ====================================
|
||||
console.log(`❌ Could not read ${logType.toLowerCase()} log: ${logPath}`);
|
||||
// </Block> =======================================
|
||||
}
|
||||
// </Block> =======================================
|
||||
}
|
||||
|
||||
// <Block> 2.1 ====================================
|
||||
export async function logs(options: OptionValues = {}): Promise<void> {
|
||||
// <Block> 2.2 ====================================
|
||||
const logsDir = PathDiscovery.getLogsDirectory();
|
||||
const tail = parseInt(options.tail) || 20;
|
||||
// </Block> =======================================
|
||||
|
||||
// Find most recent log file
|
||||
try {
|
||||
const files = readdirSync(logsDir);
|
||||
const logFiles = files
|
||||
.filter(f => f.startsWith('claude-mem-') && f.endsWith('.log'))
|
||||
.map(f => ({
|
||||
name: f,
|
||||
path: join(logsDir, f),
|
||||
mtime: statSync(join(logsDir, f)).mtime
|
||||
}))
|
||||
.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
|
||||
|
||||
if (logFiles.length === 0) {
|
||||
console.log('❌ No log files found in ~/.claude-mem/logs/');
|
||||
return;
|
||||
}
|
||||
|
||||
// Show most recent log
|
||||
await showLog(logFiles[0].path, 'Most Recent', tail);
|
||||
|
||||
if (options.all && logFiles.length > 1) {
|
||||
console.log(`📚 Found ${logFiles.length} total log files`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('❌ Could not read logs directory: ~/.claude-mem/logs/');
|
||||
console.log(' Run a compression first to generate logs');
|
||||
}
|
||||
|
||||
// <Block> 2.5 ====================================
|
||||
if (options.follow) {
|
||||
console.log('Following logs... (Press Ctrl+C to stop)');
|
||||
// Basic follow implementation - would need more sophisticated watching in real usage
|
||||
setInterval(() => {
|
||||
// This would need proper file watching implementation
|
||||
}, 1000);
|
||||
}
|
||||
// </Block> =======================================
|
||||
// </Block> =======================================
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { readdirSync, renameSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import * as p from '@clack/prompts';
|
||||
import { PathDiscovery } from '../services/path-discovery.js';
|
||||
|
||||
export async function restore(): Promise<void> {
|
||||
const trashDir = PathDiscovery.getInstance().getTrashDirectory();
|
||||
const files = readdirSync(trashDir);
|
||||
|
||||
if (files.length === 0) {
|
||||
console.log('Trash is empty');
|
||||
return;
|
||||
}
|
||||
|
||||
const file = await p.select({
|
||||
message: 'Select file to restore:',
|
||||
options: files.map(f => ({ value: f, label: f }))
|
||||
});
|
||||
|
||||
if (p.isCancel(file)) return;
|
||||
|
||||
renameSync(join(trashDir, file), join(process.cwd(), file));
|
||||
console.log(`Restored ${file}`);
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
import { readFileSync, existsSync, readdirSync, statSync } from 'fs';
|
||||
import { join, resolve, dirname } from 'path';
|
||||
import { execSync } from 'child_process';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { PathDiscovery } from '../services/path-discovery.js';
|
||||
import { DatabaseManager } from '../services/sqlite/Database.js';
|
||||
import { SessionStore } from '../services/sqlite/SessionStore.js';
|
||||
import chalk from 'chalk';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
export async function status(): Promise<void> {
|
||||
console.log('🔍 Claude Memory System Status Check');
|
||||
console.log('=====================================\n');
|
||||
|
||||
console.log('📂 Runtime Hook Scripts (installed from hook-templates/):');
|
||||
const pathDiscovery = PathDiscovery.getInstance();
|
||||
const runtimeHooksDir = pathDiscovery.getHooksDirectory();
|
||||
const sessionStartScript = join(runtimeHooksDir, 'session-start.js');
|
||||
const stopScript = join(runtimeHooksDir, 'stop.js');
|
||||
const userPromptScript = join(runtimeHooksDir, 'user-prompt-submit.js');
|
||||
const postToolScript = join(runtimeHooksDir, 'post-tool-use.js');
|
||||
|
||||
const checkScript = (path: string, name: string) => {
|
||||
if (existsSync(path)) {
|
||||
console.log(` ✅ ${name}: Found at ${path}`);
|
||||
} else {
|
||||
console.log(` ❌ ${name}: Not found at ${path}`);
|
||||
}
|
||||
};
|
||||
|
||||
checkScript(sessionStartScript, 'session-start.js');
|
||||
checkScript(stopScript, 'stop.js');
|
||||
checkScript(userPromptScript, 'user-prompt-submit.js');
|
||||
checkScript(postToolScript, 'post-tool-use.js');
|
||||
|
||||
console.log('');
|
||||
|
||||
console.log('⚙️ Settings Configuration:');
|
||||
|
||||
const checkSettings = (name: string, path: string) => {
|
||||
if (!existsSync(path)) {
|
||||
console.log(` ⏭️ ${name}: No settings file`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(` 📋 ${name}: ${path}`);
|
||||
|
||||
try {
|
||||
const settings = JSON.parse(readFileSync(path, 'utf8'));
|
||||
|
||||
const hasSessionStart = settings.hooks?.SessionStart?.some((matcher: any) =>
|
||||
matcher.hooks?.some((hook: any) =>
|
||||
hook.command?.includes('session-start.js') || hook.command?.includes('claude-mem')
|
||||
)
|
||||
);
|
||||
|
||||
const hasStop = settings.hooks?.Stop?.some((matcher: any) =>
|
||||
matcher.hooks?.some((hook: any) =>
|
||||
hook.command?.includes('stop.js') || hook.command?.includes('claude-mem')
|
||||
)
|
||||
);
|
||||
|
||||
const hasUserPrompt = settings.hooks?.UserPromptSubmit?.some((matcher: any) =>
|
||||
matcher.hooks?.some((hook: any) =>
|
||||
hook.command?.includes('user-prompt-submit.js') || hook.command?.includes('claude-mem')
|
||||
)
|
||||
);
|
||||
|
||||
const hasPostTool = settings.hooks?.PostToolUse?.some((matcher: any) =>
|
||||
matcher.hooks?.some((hook: any) =>
|
||||
hook.command?.includes('post-tool-use.js') || hook.command?.includes('claude-mem')
|
||||
)
|
||||
);
|
||||
|
||||
console.log(` SessionStart: ${hasSessionStart ? '✅' : '❌'}`);
|
||||
console.log(` Stop: ${hasStop ? '✅' : '❌'}`);
|
||||
console.log(` UserPromptSubmit: ${hasUserPrompt ? '✅' : '❌'}`);
|
||||
console.log(` PostToolUse: ${hasPostTool ? '✅' : '❌'}`);
|
||||
|
||||
} catch (error: any) {
|
||||
console.log(` ⚠️ Could not parse settings`);
|
||||
}
|
||||
};
|
||||
|
||||
checkSettings('Global', pathDiscovery.getClaudeSettingsPath());
|
||||
checkSettings('Project', join(process.cwd(), '.claude', 'settings.json'));
|
||||
|
||||
console.log('');
|
||||
|
||||
console.log('📦 Compressed Transcripts:');
|
||||
const claudeProjectsDir = join(pathDiscovery.getClaudeConfigDirectory(), 'projects');
|
||||
|
||||
if (existsSync(claudeProjectsDir)) {
|
||||
try {
|
||||
let compressedCount = 0;
|
||||
let archiveCount = 0;
|
||||
|
||||
const searchDir = (dir: string, depth = 0) => {
|
||||
if (depth > 3) return;
|
||||
|
||||
const files = readdirSync(dir);
|
||||
for (const file of files) {
|
||||
const fullPath = join(dir, file);
|
||||
const stats = statSync(fullPath);
|
||||
|
||||
if (stats.isDirectory() && !file.startsWith('.')) {
|
||||
searchDir(fullPath, depth + 1);
|
||||
} else if (file.endsWith('.jsonl.compressed')) {
|
||||
compressedCount++;
|
||||
} else if (file.endsWith('.jsonl.archive')) {
|
||||
archiveCount++;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
searchDir(claudeProjectsDir);
|
||||
|
||||
console.log(` Compressed files: ${compressedCount}`);
|
||||
console.log(` Archive files: ${archiveCount}`);
|
||||
|
||||
} catch (error) {
|
||||
console.log(` ⚠️ Could not scan projects directory`);
|
||||
}
|
||||
} else {
|
||||
console.log(` ℹ️ No Claude projects directory found`);
|
||||
}
|
||||
|
||||
console.log('');
|
||||
|
||||
console.log('🔧 Runtime Environment:');
|
||||
|
||||
const checkCommand = (cmd: string, name: string) => {
|
||||
try {
|
||||
const version = execSync(`${cmd} --version`, { encoding: 'utf8' }).trim();
|
||||
console.log(` ✅ ${name}: ${version}`);
|
||||
} catch {
|
||||
console.log(` ❌ ${name}: Not found`);
|
||||
}
|
||||
};
|
||||
|
||||
checkCommand('node', 'Node.js');
|
||||
checkCommand('bun', 'Bun');
|
||||
|
||||
console.log('');
|
||||
|
||||
console.log('🧠 Chroma Storage Status:');
|
||||
console.log(' ✅ Storage backend: Chroma MCP');
|
||||
console.log(` 📍 Data location: ${pathDiscovery.getChromaDirectory()}`);
|
||||
console.log(' 🔍 Features: Vector search, semantic similarity, document storage');
|
||||
|
||||
console.log('');
|
||||
|
||||
console.log('🤖 Claude Agent SDK Sessions:');
|
||||
try {
|
||||
const dbManager = DatabaseManager.getInstance();
|
||||
await dbManager.initialize();
|
||||
const sessionStore = new SessionStore();
|
||||
const sessions = sessionStore.getAll();
|
||||
|
||||
if (sessions.length === 0) {
|
||||
console.log(chalk.gray(' No active sessions'));
|
||||
} else {
|
||||
const activeCount = sessions.filter(s => {
|
||||
const daysSinceUse = (Date.now() - s.last_used_epoch) / (1000 * 60 * 60 * 24);
|
||||
return daysSinceUse < 7;
|
||||
}).length;
|
||||
|
||||
console.log(` 📊 Total sessions: ${sessions.length}`);
|
||||
console.log(` ✅ Active (< 7 days): ${activeCount}`);
|
||||
console.log(chalk.dim(` 💡 View details: claude-mem sessions list`));
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(chalk.gray(' ⚠️ Could not load session info'));
|
||||
}
|
||||
|
||||
console.log('');
|
||||
|
||||
console.log('📊 Summary:');
|
||||
const globalPath = pathDiscovery.getClaudeSettingsPath();
|
||||
const projectPath = join(process.cwd(), '.claude', 'settings.json');
|
||||
|
||||
let isInstalled = false;
|
||||
let installLocation = 'Not installed';
|
||||
|
||||
try {
|
||||
if (existsSync(globalPath)) {
|
||||
const settings = JSON.parse(readFileSync(globalPath, 'utf8'));
|
||||
if (settings.hooks?.SessionStart || settings.hooks?.Stop || settings.hooks?.PostToolUse) {
|
||||
isInstalled = true;
|
||||
installLocation = 'Global';
|
||||
}
|
||||
}
|
||||
|
||||
if (existsSync(projectPath)) {
|
||||
const settings = JSON.parse(readFileSync(projectPath, 'utf8'));
|
||||
if (settings.hooks?.SessionStart || settings.hooks?.Stop || settings.hooks?.PostToolUse) {
|
||||
isInstalled = true;
|
||||
installLocation = installLocation === 'Global' ? 'Global + Project' : 'Project';
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
|
||||
if (isInstalled) {
|
||||
console.log(` ✅ Claude Memory System is installed (${installLocation})`);
|
||||
console.log('');
|
||||
console.log('💡 To test: Use /compact in Claude Code');
|
||||
} else {
|
||||
console.log(` ❌ Claude Memory System is not installed`);
|
||||
console.log('');
|
||||
console.log('💡 To install: claude-mem install');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
import { OptionValues } from 'commander';
|
||||
import { spawnSync } from 'child_process';
|
||||
import { createStores } from '../services/sqlite/index.js';
|
||||
|
||||
/**
|
||||
* Store a memory to all three storage layers
|
||||
* Called by SDK via bash during streaming memory capture
|
||||
*/
|
||||
export async function storeMemory(options: OptionValues): Promise<void> {
|
||||
const { id, project, session, date, title, subtitle, facts, concepts, files } = options;
|
||||
|
||||
// Validate required fields
|
||||
if (!id || !project || !session || !date) {
|
||||
console.error('Error: All fields required: --id, --project, --session, --date');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Validate hierarchical fields (required for v2 format)
|
||||
if (!title || !subtitle || !facts) {
|
||||
console.error('Error: Hierarchical format required: --title, --subtitle, --facts');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
const stores = await createStores();
|
||||
const timestamp = new Date().toISOString();
|
||||
|
||||
// Ensure session exists
|
||||
const sessionExists = await stores.sessions.has(session);
|
||||
if (!sessionExists) {
|
||||
await stores.sessions.create({
|
||||
session_id: session,
|
||||
project,
|
||||
created_at: timestamp,
|
||||
source: 'save'
|
||||
});
|
||||
}
|
||||
|
||||
// Parse JSON arrays if provided as strings
|
||||
let factsArray: string | undefined;
|
||||
let conceptsArray: string | undefined;
|
||||
let filesArray: string | undefined;
|
||||
|
||||
try {
|
||||
factsArray = facts ? JSON.stringify(JSON.parse(facts)) : undefined;
|
||||
} catch (e) {
|
||||
factsArray = facts; // Store as-is if not valid JSON
|
||||
}
|
||||
|
||||
try {
|
||||
conceptsArray = concepts ? JSON.stringify(JSON.parse(concepts)) : undefined;
|
||||
} catch (e) {
|
||||
conceptsArray = concepts; // Store as-is if not valid JSON
|
||||
}
|
||||
|
||||
try {
|
||||
filesArray = files ? JSON.stringify(JSON.parse(files)) : undefined;
|
||||
} catch (e) {
|
||||
filesArray = files; // Store as-is if not valid JSON
|
||||
}
|
||||
|
||||
// Layer 1: SQLite Memory Index
|
||||
const memoryExists = stores.memories.hasDocumentId(id);
|
||||
if (!memoryExists) {
|
||||
stores.memories.create({
|
||||
document_id: id,
|
||||
text: '', // Deprecated: hierarchical fields replace narrative text
|
||||
keywords: '',
|
||||
session_id: session,
|
||||
project,
|
||||
created_at: timestamp,
|
||||
origin: 'streaming-sdk',
|
||||
// Hierarchical fields (v2)
|
||||
title: title || undefined,
|
||||
subtitle: subtitle || undefined,
|
||||
facts: factsArray,
|
||||
concepts: conceptsArray,
|
||||
files_touched: filesArray
|
||||
});
|
||||
}
|
||||
|
||||
// Layer 2: ChromaDB - Store hierarchical memory
|
||||
if (factsArray) {
|
||||
const factsJson = JSON.parse(factsArray);
|
||||
const conceptsJson = conceptsArray ? JSON.parse(conceptsArray) : [];
|
||||
const filesJson = filesArray ? JSON.parse(filesArray) : [];
|
||||
|
||||
// Store each atomic fact as a separate ChromaDB document
|
||||
factsJson.forEach((fact: string, idx: number) => {
|
||||
spawnSync('claude-mem', [
|
||||
'chroma_add_documents',
|
||||
'--collection_name', 'claude_memories',
|
||||
'--documents', JSON.stringify([fact]),
|
||||
'--ids', JSON.stringify([`${id}_fact_${String(idx).padStart(3, '0')}`]),
|
||||
'--metadatas', JSON.stringify([{
|
||||
type: 'fact',
|
||||
parent_id: id,
|
||||
fact_index: idx,
|
||||
title,
|
||||
subtitle,
|
||||
project,
|
||||
session_id: session,
|
||||
created_at: timestamp,
|
||||
created_at_epoch: Date.parse(timestamp),
|
||||
keywords: '',
|
||||
concepts: JSON.stringify(conceptsJson),
|
||||
files_touched: JSON.stringify(filesJson),
|
||||
origin: 'streaming-sdk'
|
||||
}])
|
||||
]);
|
||||
});
|
||||
|
||||
// Store full narrative with hierarchical metadata
|
||||
spawnSync('claude-mem', [
|
||||
'chroma_add_documents',
|
||||
'--collection_name', 'claude_memories',
|
||||
'--documents', JSON.stringify([`${title}\n${subtitle}\n\n${factsJson.join('\n')}`]),
|
||||
'--ids', JSON.stringify([id]),
|
||||
'--metadatas', JSON.stringify([{
|
||||
type: 'narrative',
|
||||
title,
|
||||
subtitle,
|
||||
facts_count: factsJson.length,
|
||||
project,
|
||||
session_id: session,
|
||||
created_at: timestamp,
|
||||
created_at_epoch: Date.parse(timestamp),
|
||||
keywords: '',
|
||||
concepts: JSON.stringify(conceptsJson),
|
||||
files_touched: JSON.stringify(filesJson),
|
||||
origin: 'streaming-sdk'
|
||||
}])
|
||||
]);
|
||||
}
|
||||
|
||||
// Success output (SDK will see this)
|
||||
console.log(JSON.stringify({
|
||||
success: true,
|
||||
memory_id: id,
|
||||
project,
|
||||
session,
|
||||
date,
|
||||
timestamp,
|
||||
hierarchical: !!(title && subtitle && facts)
|
||||
}));
|
||||
|
||||
} catch (error: any) {
|
||||
console.error(JSON.stringify({
|
||||
success: false,
|
||||
error: error.message || 'Unknown error storing memory'
|
||||
}));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { OptionValues } from 'commander';
|
||||
import { createStores } from '../services/sqlite/index.js';
|
||||
|
||||
/**
|
||||
* Store a session overview
|
||||
* Called by SDK via bash at session end
|
||||
*/
|
||||
export async function storeOverview(options: OptionValues): Promise<void> {
|
||||
const { project, session, content } = options;
|
||||
|
||||
// Validate required fields
|
||||
if (!project || !session || !content) {
|
||||
console.error('Error: All fields required: --project, --session, --content');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
const stores = await createStores();
|
||||
const timestamp = new Date().toISOString();
|
||||
|
||||
// Create one overview per session (rolling log architecture)
|
||||
stores.overviews.upsert({
|
||||
session_id: session,
|
||||
content,
|
||||
created_at: timestamp,
|
||||
project,
|
||||
origin: 'streaming-sdk'
|
||||
});
|
||||
|
||||
// Success output (SDK will see this)
|
||||
console.log(JSON.stringify({
|
||||
success: true,
|
||||
project,
|
||||
session,
|
||||
timestamp
|
||||
}));
|
||||
|
||||
} catch (error: any) {
|
||||
console.error(JSON.stringify({
|
||||
success: false,
|
||||
error: error.message || 'Unknown error storing overview'
|
||||
}));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { rmSync, readdirSync, existsSync, statSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import * as p from '@clack/prompts';
|
||||
import { PathDiscovery } from '../services/path-discovery.js';
|
||||
|
||||
export async function emptyTrash(options: { force?: boolean } = {}): Promise<void> {
|
||||
const trashDir = PathDiscovery.getInstance().getTrashDirectory();
|
||||
|
||||
// Check if trash directory exists
|
||||
if (!existsSync(trashDir)) {
|
||||
p.log.info('🗑️ Trash is already empty');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const files = readdirSync(trashDir);
|
||||
|
||||
if (files.length === 0) {
|
||||
p.log.info('🗑️ Trash is already empty');
|
||||
return;
|
||||
}
|
||||
|
||||
// Count items
|
||||
let folderCount = 0;
|
||||
let fileCount = 0;
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = join(trashDir, file);
|
||||
const stats = statSync(filePath);
|
||||
if (stats.isDirectory()) {
|
||||
folderCount++;
|
||||
} else {
|
||||
fileCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// Confirm deletion unless --force flag is used
|
||||
if (!options.force) {
|
||||
const confirm = await p.confirm({
|
||||
message: `Permanently delete ${folderCount} folders and ${fileCount} files from trash?`,
|
||||
initialValue: false
|
||||
});
|
||||
|
||||
if (p.isCancel(confirm) || !confirm) {
|
||||
p.log.info('Cancelled - trash not emptied');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Delete all files in trash
|
||||
const s = p.spinner();
|
||||
s.start('Emptying trash...');
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = join(trashDir, file);
|
||||
rmSync(filePath, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
s.stop(`🗑️ Trash emptied - permanently deleted ${folderCount} folders and ${fileCount} files`);
|
||||
|
||||
} catch (error) {
|
||||
p.log.error('Failed to empty trash');
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
import { readdirSync, statSync } from 'fs';
|
||||
import { join, basename } from 'path';
|
||||
import * as p from '@clack/prompts';
|
||||
import { PathDiscovery } from '../services/path-discovery.js';
|
||||
|
||||
interface TrashItem {
|
||||
originalName: string;
|
||||
trashedName: string;
|
||||
size: number;
|
||||
trashedAt: Date;
|
||||
isDirectory: boolean;
|
||||
}
|
||||
|
||||
function parseTrashName(filename: string): { name: string; timestamp: number } {
|
||||
const lastDotIndex = filename.lastIndexOf('.');
|
||||
if (lastDotIndex === -1) return { name: filename, timestamp: 0 };
|
||||
|
||||
const timestamp = parseInt(filename.substring(lastDotIndex + 1));
|
||||
if (isNaN(timestamp)) return { name: filename, timestamp: 0 };
|
||||
|
||||
return {
|
||||
name: filename.substring(0, lastDotIndex),
|
||||
timestamp
|
||||
};
|
||||
}
|
||||
|
||||
function formatSize(bytes: number): string {
|
||||
if (bytes === 0) return '0 B';
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
function getDirectorySize(dirPath: string): number {
|
||||
let size = 0;
|
||||
const files = readdirSync(dirPath);
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = join(dirPath, file);
|
||||
const stats = statSync(filePath);
|
||||
|
||||
if (stats.isDirectory()) {
|
||||
size += getDirectorySize(filePath);
|
||||
} else {
|
||||
size += stats.size;
|
||||
}
|
||||
}
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
export async function viewTrash(): Promise<void> {
|
||||
const trashDir = PathDiscovery.getInstance().getTrashDirectory();
|
||||
|
||||
try {
|
||||
const files = readdirSync(trashDir);
|
||||
|
||||
if (files.length === 0) {
|
||||
p.log.info('🗑️ Trash is empty');
|
||||
return;
|
||||
}
|
||||
|
||||
const items: TrashItem[] = files.map(file => {
|
||||
const filePath = join(trashDir, file);
|
||||
const stats = statSync(filePath);
|
||||
const { name, timestamp } = parseTrashName(file);
|
||||
|
||||
const size = stats.isDirectory() ? getDirectorySize(filePath) : stats.size;
|
||||
|
||||
return {
|
||||
originalName: name,
|
||||
trashedName: file,
|
||||
size,
|
||||
trashedAt: new Date(timestamp),
|
||||
isDirectory: stats.isDirectory()
|
||||
};
|
||||
});
|
||||
|
||||
// Sort by date, newest first
|
||||
items.sort((a, b) => b.trashedAt.getTime() - a.trashedAt.getTime());
|
||||
|
||||
// Display header
|
||||
console.log('\n🗑️ Trash Contents\n');
|
||||
console.log('─'.repeat(80));
|
||||
|
||||
// Display items
|
||||
let totalSize = 0;
|
||||
let folderCount = 0;
|
||||
let fileCount = 0;
|
||||
|
||||
for (const item of items) {
|
||||
totalSize += item.size;
|
||||
if (item.isDirectory) {
|
||||
folderCount++;
|
||||
} else {
|
||||
fileCount++;
|
||||
}
|
||||
|
||||
const type = item.isDirectory ? '📁' : '📄';
|
||||
const date = item.trashedAt.toLocaleString();
|
||||
const size = formatSize(item.size);
|
||||
|
||||
console.log(`${type} ${item.originalName}`);
|
||||
console.log(` Size: ${size} | Trashed: ${date}`);
|
||||
console.log(` ID: ${item.trashedName}`);
|
||||
console.log();
|
||||
}
|
||||
|
||||
// Display summary
|
||||
console.log('─'.repeat(80));
|
||||
console.log(`Total: ${folderCount} folders, ${fileCount} files (${formatSize(totalSize)})`);
|
||||
console.log('\nTo restore files: claude-mem restore');
|
||||
console.log('To empty trash: claude-mem trash empty');
|
||||
|
||||
} catch (error) {
|
||||
if ((error as any).code === 'ENOENT') {
|
||||
p.log.info('🗑️ Trash is empty');
|
||||
} else {
|
||||
p.log.error('Failed to read trash directory');
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { renameSync, existsSync, mkdirSync, statSync } from 'fs';
|
||||
import { join, basename } from 'path';
|
||||
import { glob } from 'glob';
|
||||
import { PathDiscovery } from '../services/path-discovery.js';
|
||||
|
||||
interface TrashOptions {
|
||||
force?: boolean;
|
||||
recursive?: boolean;
|
||||
}
|
||||
|
||||
export async function trash(filePaths: string | string[], options: TrashOptions = {}): Promise<void> {
|
||||
const trashDir = PathDiscovery.getInstance().getTrashDirectory();
|
||||
if (!existsSync(trashDir)) mkdirSync(trashDir, { recursive: true });
|
||||
|
||||
// Handle single string or array of paths
|
||||
const paths = Array.isArray(filePaths) ? filePaths : [filePaths];
|
||||
|
||||
for (const filePath of paths) {
|
||||
// Handle glob patterns
|
||||
const expandedPaths = await glob(filePath);
|
||||
const actualPaths = expandedPaths.length > 0 ? expandedPaths : [filePath];
|
||||
|
||||
for (const actualPath of actualPaths) {
|
||||
try {
|
||||
// Check if file exists
|
||||
if (!existsSync(actualPath)) {
|
||||
if (!options.force) {
|
||||
console.error(`trash: ${actualPath}: No such file or directory`);
|
||||
continue;
|
||||
}
|
||||
// With -f, silently skip missing files
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if it's a directory and we need recursive
|
||||
const stats = statSync(actualPath);
|
||||
if (stats.isDirectory() && !options.recursive) {
|
||||
if (!options.force) {
|
||||
console.error(`trash: ${actualPath}: is a directory`);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Generate unique destination name to avoid conflicts
|
||||
const fileName = basename(actualPath);
|
||||
const timestamp = Date.now();
|
||||
const destination = join(trashDir, `${fileName}.${timestamp}`);
|
||||
|
||||
renameSync(actualPath, destination);
|
||||
console.log(`Moved ${fileName} to trash`);
|
||||
|
||||
} catch (error) {
|
||||
if (!options.force) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
console.error(`trash: ${actualPath}: ${errorMessage}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
import { OptionValues } from 'commander';
|
||||
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { homedir } from 'os';
|
||||
import { PathDiscovery } from '../services/path-discovery.js';
|
||||
|
||||
async function removeSmartTrashAlias(): Promise<boolean> {
|
||||
const homeDir = homedir();
|
||||
const shellConfigs = [
|
||||
join(homeDir, '.bashrc'),
|
||||
join(homeDir, '.zshrc'),
|
||||
join(homeDir, '.bash_profile')
|
||||
];
|
||||
|
||||
const aliasLine = 'alias rm="claude-mem trash"';
|
||||
// Handle both variations of the comment line
|
||||
const commentPatterns = [
|
||||
'# claude-mem smart trash alias',
|
||||
'# claude-mem trash bin alias'
|
||||
];
|
||||
let removedFromAny = false;
|
||||
|
||||
for (const configPath of shellConfigs) {
|
||||
if (!existsSync(configPath)) continue;
|
||||
|
||||
let content = readFileSync(configPath, 'utf8');
|
||||
|
||||
// Check if alias exists
|
||||
if (!content.includes(aliasLine)) {
|
||||
continue; // Not configured in this file
|
||||
}
|
||||
|
||||
// Remove the alias and its comment
|
||||
const lines = content.split('\n');
|
||||
const filteredLines = lines.filter((line, index) => {
|
||||
// Skip the alias line
|
||||
if (line.trim() === aliasLine) return false;
|
||||
// Skip any claude-mem comment line if it's right before the alias
|
||||
for (const commentPattern of commentPatterns) {
|
||||
if (line.trim() === commentPattern &&
|
||||
index + 1 < lines.length &&
|
||||
lines[index + 1].trim() === aliasLine) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
const newContent = filteredLines.join('\n');
|
||||
|
||||
// Only write if content actually changed
|
||||
if (newContent !== content) {
|
||||
// Create backup
|
||||
const backupPath = configPath + '.backup.' + Date.now();
|
||||
writeFileSync(backupPath, content);
|
||||
|
||||
// Write updated content
|
||||
writeFileSync(configPath, newContent);
|
||||
console.log(`✅ Removed Smart Trash alias from ${configPath.replace(homeDir, '~')}`);
|
||||
removedFromAny = true;
|
||||
}
|
||||
}
|
||||
|
||||
return removedFromAny;
|
||||
}
|
||||
|
||||
export async function uninstall(options: OptionValues = {}): Promise<void> {
|
||||
console.log('🔄 Uninstalling Claude Memory System hooks...');
|
||||
|
||||
const locations = [];
|
||||
if (options.all) {
|
||||
locations.push({
|
||||
name: 'User',
|
||||
path: PathDiscovery.getInstance().getClaudeSettingsPath()
|
||||
});
|
||||
locations.push({
|
||||
name: 'Project',
|
||||
path: join(process.cwd(), '.claude', 'settings.json')
|
||||
});
|
||||
} else {
|
||||
const isProject = options.project;
|
||||
const pathDiscovery = PathDiscovery.getInstance();
|
||||
locations.push({
|
||||
name: isProject ? 'Project' : 'User',
|
||||
path: isProject ? join(process.cwd(), '.claude', 'settings.json') : pathDiscovery.getClaudeSettingsPath()
|
||||
});
|
||||
}
|
||||
|
||||
const pathDiscovery = PathDiscovery.getInstance();
|
||||
const runtimeHooksDir = pathDiscovery.getHooksDirectory();
|
||||
const preCompactScript = join(runtimeHooksDir, 'pre-compact.js');
|
||||
const sessionStartScript = join(runtimeHooksDir, 'session-start.js');
|
||||
const sessionEndScript = join(runtimeHooksDir, 'session-end.js');
|
||||
|
||||
let removedCount = 0;
|
||||
|
||||
for (const location of locations) {
|
||||
if (!existsSync(location.path)) {
|
||||
console.log(`⏭️ No settings found at ${location.name} location`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const content = readFileSync(location.path, 'utf8');
|
||||
const settings = JSON.parse(content);
|
||||
|
||||
if (!settings.hooks) {
|
||||
console.log(`⏭️ No hooks configured in ${location.name} settings`);
|
||||
continue;
|
||||
}
|
||||
|
||||
let modified = false;
|
||||
|
||||
if (settings.hooks.PreCompact) {
|
||||
const filteredPreCompact = settings.hooks.PreCompact.filter((matcher: any) =>
|
||||
!matcher.hooks?.some((hook: any) =>
|
||||
hook.command === preCompactScript ||
|
||||
hook.command?.includes('pre-compact.js') ||
|
||||
hook.command?.includes('claude-mem')
|
||||
)
|
||||
);
|
||||
|
||||
if (filteredPreCompact.length !== settings.hooks.PreCompact.length) {
|
||||
settings.hooks.PreCompact = filteredPreCompact.length ? filteredPreCompact : undefined;
|
||||
modified = true;
|
||||
console.log(`✅ Removed PreCompact hook from ${location.name} settings`);
|
||||
}
|
||||
}
|
||||
|
||||
if (settings.hooks.SessionStart) {
|
||||
const filteredSessionStart = settings.hooks.SessionStart.filter((matcher: any) =>
|
||||
!matcher.hooks?.some((hook: any) =>
|
||||
hook.command === sessionStartScript ||
|
||||
hook.command?.includes('session-start.js') ||
|
||||
hook.command?.includes('claude-mem')
|
||||
)
|
||||
);
|
||||
|
||||
if (filteredSessionStart.length !== settings.hooks.SessionStart.length) {
|
||||
settings.hooks.SessionStart = filteredSessionStart.length ? filteredSessionStart : undefined;
|
||||
modified = true;
|
||||
console.log(`✅ Removed SessionStart hook from ${location.name} settings`);
|
||||
}
|
||||
}
|
||||
|
||||
if (settings.hooks.SessionEnd) {
|
||||
const filteredSessionEnd = settings.hooks.SessionEnd.filter((matcher: any) =>
|
||||
!matcher.hooks?.some((hook: any) =>
|
||||
hook.command === sessionEndScript ||
|
||||
hook.command?.includes('session-end.js') ||
|
||||
hook.command?.includes('claude-mem')
|
||||
)
|
||||
);
|
||||
|
||||
if (filteredSessionEnd.length !== settings.hooks.SessionEnd.length) {
|
||||
settings.hooks.SessionEnd = filteredSessionEnd.length ? filteredSessionEnd : undefined;
|
||||
modified = true;
|
||||
console.log(`✅ Removed SessionEnd hook from ${location.name} settings`);
|
||||
}
|
||||
}
|
||||
|
||||
if (settings.hooks.PreCompact === undefined) delete settings.hooks.PreCompact;
|
||||
if (settings.hooks.SessionStart === undefined) delete settings.hooks.SessionStart;
|
||||
if (settings.hooks.SessionEnd === undefined) delete settings.hooks.SessionEnd;
|
||||
if (!Object.keys(settings.hooks).length) delete settings.hooks;
|
||||
|
||||
if (modified) {
|
||||
const backupPath = location.path + '.backup.' + Date.now();
|
||||
writeFileSync(backupPath, content);
|
||||
console.log(`📋 Created backup: ${backupPath}`);
|
||||
|
||||
writeFileSync(location.path, JSON.stringify(settings, null, 2));
|
||||
removedCount++;
|
||||
console.log(`✅ Updated ${location.name} settings: ${location.path}`);
|
||||
} else {
|
||||
console.log(`ℹ️ No Claude Memory System hooks found in ${location.name} settings`);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove Smart Trash alias from shell configs
|
||||
const removedAlias = await removeSmartTrashAlias();
|
||||
|
||||
console.log('');
|
||||
if (removedCount > 0 || removedAlias) {
|
||||
console.log('✨ Uninstallation complete!');
|
||||
if (removedCount > 0) {
|
||||
console.log('The Claude Memory System hooks have been removed from your settings.');
|
||||
}
|
||||
if (removedAlias) {
|
||||
console.log('The Smart Trash alias has been removed from your shell configuration.');
|
||||
console.log('⚠️ Restart your terminal for the alias removal to take effect.');
|
||||
}
|
||||
console.log('');
|
||||
console.log('Note: Your compressed transcripts and archives are preserved.');
|
||||
console.log('To reinstall: claude-mem install');
|
||||
} else {
|
||||
console.log('ℹ️ No Claude Memory System hooks or aliases were found to remove.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import { OptionValues } from 'commander';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
|
||||
const SESSION_DIR = path.join(process.env.HOME || '', '.claude-mem', 'sessions');
|
||||
|
||||
/**
|
||||
* Update session metadata (title/subtitle) in the streaming session JSON file
|
||||
* Called by SDK when generating session title at the start
|
||||
*/
|
||||
export async function updateSessionMetadata(options: OptionValues): Promise<void> {
|
||||
const { project, session, title, subtitle } = options;
|
||||
|
||||
// Validate required fields
|
||||
if (!project || !session) {
|
||||
console.error(JSON.stringify({
|
||||
success: false,
|
||||
error: 'Missing required fields: --project, --session'
|
||||
}));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!title) {
|
||||
console.error(JSON.stringify({
|
||||
success: false,
|
||||
error: 'Missing required field: --title'
|
||||
}));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
// Load existing session file
|
||||
const sessionFile = path.join(SESSION_DIR, `${project}_streaming.json`);
|
||||
|
||||
if (!fs.existsSync(sessionFile)) {
|
||||
console.error(JSON.stringify({
|
||||
success: false,
|
||||
error: `Session file not found: ${sessionFile}`
|
||||
}));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let sessionData: any = {};
|
||||
try {
|
||||
sessionData = JSON.parse(fs.readFileSync(sessionFile, 'utf8'));
|
||||
} catch (e) {
|
||||
console.error(JSON.stringify({
|
||||
success: false,
|
||||
error: 'Failed to parse session file'
|
||||
}));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Update metadata
|
||||
sessionData.promptTitle = title;
|
||||
if (subtitle) {
|
||||
sessionData.promptSubtitle = subtitle;
|
||||
}
|
||||
sessionData.updatedAt = new Date().toISOString();
|
||||
|
||||
// Write back to file
|
||||
fs.writeFileSync(sessionFile, JSON.stringify(sessionData, null, 2));
|
||||
|
||||
// Output success
|
||||
console.log(JSON.stringify({
|
||||
success: true,
|
||||
title,
|
||||
subtitle: subtitle || null,
|
||||
project,
|
||||
session
|
||||
}));
|
||||
|
||||
} catch (error: any) {
|
||||
console.error(JSON.stringify({
|
||||
success: false,
|
||||
error: error.message || 'Unknown error updating session metadata'
|
||||
}));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Claude Memory System - Core Constants
|
||||
*
|
||||
* This file contains debug logging templates used throughout the application.
|
||||
*/
|
||||
|
||||
// =============================================================================
|
||||
// DEBUG AND LOGGING TEMPLATES
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Debug logging message templates
|
||||
*/
|
||||
export const DEBUG_MESSAGES = {
|
||||
COMPRESSION_STARTED: '🚀 COMPRESSION STARTED',
|
||||
TRANSCRIPT_PATH: (path: string) => `📁 Transcript Path: ${path}`,
|
||||
SESSION_ID: (id: string) => `🔍 Session ID: ${id}`,
|
||||
PROJECT_NAME: (name: string) => `📝 PROJECT NAME: ${name}`,
|
||||
CLAUDE_SDK_CALL: '🤖 Calling Claude SDK to analyze and populate memory database...',
|
||||
TRANSCRIPT_STATS: (size: number, count: number) =>
|
||||
`📊 Transcript size: ${size} characters, ${count} messages`,
|
||||
COMPRESSION_COMPLETE: (count: number) => `✅ COMPRESSION COMPLETE\n Total summaries extracted: ${count}`,
|
||||
CLAUDE_PATH_FOUND: (path: string) => `🎯 Found Claude Code at: ${path}`,
|
||||
MCP_CONFIG_USED: (path: string) => `📋 Using MCP config: ${path}`
|
||||
} as const;
|
||||
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* Time utilities for formatting relative timestamps
|
||||
*/
|
||||
|
||||
export function formatRelativeTime(timestamp: string | Date): string {
|
||||
try {
|
||||
const date = timestamp instanceof Date ? timestamp : new Date(timestamp);
|
||||
const now = new Date();
|
||||
const diffMs = now.getTime() - date.getTime();
|
||||
const diffSeconds = Math.floor(diffMs / 1000);
|
||||
const diffMinutes = Math.floor(diffSeconds / 60);
|
||||
const diffHours = Math.floor(diffMinutes / 60);
|
||||
const diffDays = Math.floor(diffHours / 24);
|
||||
const diffWeeks = Math.floor(diffDays / 7);
|
||||
const diffMonths = Math.floor(diffDays / 30);
|
||||
|
||||
if (diffSeconds < 60) {
|
||||
return 'Just now';
|
||||
} else if (diffMinutes < 60) {
|
||||
return diffMinutes === 1 ? '1 minute ago' : `${diffMinutes} minutes ago`;
|
||||
} else if (diffHours < 24) {
|
||||
return diffHours === 1 ? '1 hour ago' : `${diffHours} hours ago`;
|
||||
} else if (diffDays === 1) {
|
||||
return 'Yesterday';
|
||||
} else if (diffDays < 7) {
|
||||
return `${diffDays} days ago`;
|
||||
} else if (diffWeeks === 1) {
|
||||
return '1 week ago';
|
||||
} else if (diffWeeks < 4) {
|
||||
return `${diffWeeks} weeks ago`;
|
||||
} else if (diffMonths === 1) {
|
||||
return '1 month ago';
|
||||
} else if (diffMonths < 12) {
|
||||
return `${diffMonths} months ago`;
|
||||
} else {
|
||||
const diffYears = Math.floor(diffMonths / 12);
|
||||
return diffYears === 1 ? '1 year ago' : `${diffYears} years ago`;
|
||||
}
|
||||
} catch (error) {
|
||||
// Return a fallback for invalid timestamps
|
||||
return 'Recently';
|
||||
}
|
||||
}
|
||||
|
||||
export function parseTimestamp(entry: any): Date | null {
|
||||
// Try multiple timestamp fields that might exist
|
||||
const possibleFields = ['timestamp', 'created_at', 'date', 'time'];
|
||||
|
||||
for (const field of possibleFields) {
|
||||
if (entry[field]) {
|
||||
try {
|
||||
const date = new Date(entry[field]);
|
||||
if (!isNaN(date.getTime())) {
|
||||
return date;
|
||||
}
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If no valid timestamp found, return null
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
# Hook Prompts System
|
||||
|
||||
This directory contains the centralized prompt configuration for all streaming hooks.
|
||||
|
||||
## Quick Edit Guide
|
||||
|
||||
**Want to change hook prompts?** Edit this file:
|
||||
```
|
||||
hook-prompts.config.ts
|
||||
```
|
||||
|
||||
Then rebuild and reinstall:
|
||||
```bash
|
||||
bun run build
|
||||
bun run dev:install
|
||||
```
|
||||
|
||||
## Files in This Directory
|
||||
|
||||
### hook-prompts.config.ts
|
||||
**EDIT THIS FILE** to change prompt content.
|
||||
|
||||
Contains:
|
||||
- `SYSTEM_PROMPT` - Initial instructions for SDK (190 lines)
|
||||
- `TOOL_MESSAGE` - Format for tool responses (10 lines)
|
||||
- `END_MESSAGE` - Session completion request (10 lines)
|
||||
- `HOOK_CONFIG` - Shared settings (truncation limits, SDK options)
|
||||
|
||||
Uses `{{variableName}}` template syntax.
|
||||
|
||||
### hook-prompt-renderer.ts
|
||||
**DON'T EDIT** unless adding new variables or changing rendering logic.
|
||||
|
||||
Contains:
|
||||
- `renderSystemPrompt()` - Processes system prompt template
|
||||
- `renderToolMessage()` - Processes tool message template
|
||||
- `renderEndMessage()` - Processes end message template
|
||||
- Template substitution and auto-truncation logic
|
||||
|
||||
### templates/context/ContextTemplates.ts
|
||||
Session start message formatting (separate from hook prompts).
|
||||
|
||||
## Template Variables Reference
|
||||
|
||||
### SYSTEM_PROMPT Variables
|
||||
```typescript
|
||||
{
|
||||
project: string; // Project name (e.g., "claude-mem-source")
|
||||
sessionId: string; // Claude Code session ID
|
||||
date: string; // YYYY-MM-DD format
|
||||
userPrompt: string; // Auto-truncated to 200 chars
|
||||
}
|
||||
```
|
||||
|
||||
### TOOL_MESSAGE Variables
|
||||
```typescript
|
||||
{
|
||||
toolName: string; // Tool name (e.g., "Read", "Bash")
|
||||
toolResponse: string; // Auto-truncated to 20000 chars
|
||||
userPrompt: string; // Auto-truncated to 200 chars
|
||||
timestamp: string; // Full ISO timestamp
|
||||
timeFormatted: string; // HH:MM:SS format (auto-generated)
|
||||
}
|
||||
```
|
||||
|
||||
### END_MESSAGE Variables
|
||||
```typescript
|
||||
{
|
||||
project: string; // Project name
|
||||
sessionId: string; // Claude Code session ID
|
||||
}
|
||||
```
|
||||
|
||||
## Usage in Hooks
|
||||
|
||||
### user-prompt-submit-streaming.js
|
||||
```javascript
|
||||
import { renderSystemPrompt, HOOK_CONFIG } from '../src/prompts/hook-prompt-renderer.js';
|
||||
|
||||
const prompt = renderSystemPrompt({
|
||||
project,
|
||||
sessionId: session_id,
|
||||
date,
|
||||
userPrompt: prompt || ''
|
||||
});
|
||||
|
||||
query({
|
||||
prompt,
|
||||
options: {
|
||||
model: HOOK_CONFIG.sdk.model,
|
||||
allowedTools: HOOK_CONFIG.sdk.allowedTools,
|
||||
maxTokens: HOOK_CONFIG.sdk.maxTokensSystem
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### post-tool-use-streaming.js
|
||||
```javascript
|
||||
import { renderToolMessage, HOOK_CONFIG } from '../src/prompts/hook-prompt-renderer.js';
|
||||
|
||||
const message = renderToolMessage({
|
||||
toolName: tool_name,
|
||||
toolResponse: toolResponseStr,
|
||||
userPrompt: prompt || '',
|
||||
timestamp: timestamp || new Date().toISOString()
|
||||
});
|
||||
|
||||
query({
|
||||
prompt: message,
|
||||
options: {
|
||||
model: HOOK_CONFIG.sdk.model,
|
||||
maxTokens: HOOK_CONFIG.sdk.maxTokensTool
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### stop-streaming.js
|
||||
```javascript
|
||||
import { renderEndMessage, HOOK_CONFIG } from '../src/prompts/hook-prompt-renderer.js';
|
||||
|
||||
const message = renderEndMessage({
|
||||
project,
|
||||
sessionId: claudeSessionId
|
||||
});
|
||||
|
||||
query({
|
||||
prompt: message,
|
||||
options: {
|
||||
model: HOOK_CONFIG.sdk.model,
|
||||
maxTokens: HOOK_CONFIG.sdk.maxTokensEnd
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Configuration Options
|
||||
|
||||
Edit `HOOK_CONFIG` in `hook-prompts.config.ts`:
|
||||
|
||||
```typescript
|
||||
export const HOOK_CONFIG = {
|
||||
// Truncation limits for template variables
|
||||
maxUserPromptLength: 200, // Increase to show more context
|
||||
maxToolResponseLength: 20000, // Increase for larger outputs
|
||||
|
||||
// SDK configuration
|
||||
sdk: {
|
||||
model: 'claude-sonnet-4-5', // Change model version
|
||||
allowedTools: ['Bash'], // Add more tools if needed
|
||||
maxTokensSystem: 8192, // Token limit for system prompt
|
||||
maxTokensTool: 8192, // Token limit for tool messages
|
||||
maxTokensEnd: 2048, // Token limit for end message
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
## Example: Editing a Prompt
|
||||
|
||||
### Before
|
||||
```typescript
|
||||
export const TOOL_MESSAGE = `# Tool Response {{timeFormatted}}
|
||||
|
||||
Tool: {{toolName}}
|
||||
User Context: "{{userPrompt}}"
|
||||
|
||||
\`\`\`
|
||||
{{toolResponse}}
|
||||
\`\`\`
|
||||
|
||||
Analyze and store if meaningful.`;
|
||||
```
|
||||
|
||||
### After
|
||||
```typescript
|
||||
export const TOOL_MESSAGE = `# Analysis Request {{timeFormatted}}
|
||||
|
||||
Executed: {{toolName}}
|
||||
Context: "{{userPrompt}}"
|
||||
Priority: High
|
||||
|
||||
Output:
|
||||
\`\`\`
|
||||
{{toolResponse}}
|
||||
\`\`\`
|
||||
|
||||
IMPORTANT: Only store if this contains:
|
||||
- New code patterns or logic
|
||||
- Architecture decisions
|
||||
- Error messages with solutions
|
||||
- Configuration changes
|
||||
|
||||
Skip trivial operations.`;
|
||||
```
|
||||
|
||||
### Apply Changes
|
||||
```bash
|
||||
bun run build && bun run dev:install
|
||||
```
|
||||
|
||||
## Benefits
|
||||
|
||||
### DRY Compliance
|
||||
- **Before**: 3 files with 188 lines of hardcoded prompts
|
||||
- **After**: 1 config file with all prompts centralized
|
||||
|
||||
### Maintainability
|
||||
- Change prompts without touching hook implementation
|
||||
- Type-safe template variables
|
||||
- Consistent formatting across all hooks
|
||||
- Version-controlled prompt history
|
||||
|
||||
### Flexibility
|
||||
- Easy A/B testing of different instructions
|
||||
- Simple to adjust truncation limits
|
||||
- Quick model/token configuration changes
|
||||
- Template variables prevent copy-paste errors
|
||||
|
||||
## Full Documentation
|
||||
|
||||
See `/Users/alexnewman/Scripts/claude-mem-source/docs/HOOK_PROMPTS.md` for:
|
||||
- Detailed editing guide
|
||||
- Troubleshooting common issues
|
||||
- Adding new template variables
|
||||
- Advanced customization
|
||||
- Migration notes
|
||||
@@ -0,0 +1,159 @@
|
||||
/**
|
||||
* Hook Prompt Renderer
|
||||
*
|
||||
* Simple template rendering for hook prompts.
|
||||
* Handles variable substitution and auto-truncation.
|
||||
*/
|
||||
|
||||
import {
|
||||
PROMPTS,
|
||||
HOOK_CONFIG,
|
||||
type SystemPromptVariables,
|
||||
type ToolMessageVariables,
|
||||
type EndMessageVariables,
|
||||
} from './hook-prompts.config.js';
|
||||
|
||||
// =============================================================================
|
||||
// TEMPLATE RENDERING
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Simple template variable substitution
|
||||
* Replaces {{variableName}} with actual values
|
||||
*/
|
||||
function substituteVariables(
|
||||
template: string,
|
||||
variables: Record<string, string>
|
||||
): string {
|
||||
let result = template;
|
||||
|
||||
for (const [key, value] of Object.entries(variables)) {
|
||||
const placeholder = `{{${key}}}`;
|
||||
// Replace all occurrences of this placeholder
|
||||
result = result.split(placeholder).join(value);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Truncate text with ellipsis if it exceeds maxLength
|
||||
*/
|
||||
function truncate(text: string, maxLength: number): string {
|
||||
if (text.length <= maxLength) return text;
|
||||
return text.slice(0, maxLength) + (text.length > maxLength ? '...' : '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Format timestamp for tool message header
|
||||
* Extracts HH:MM:SS from ISO timestamp
|
||||
*/
|
||||
function formatTime(timestamp: string): string {
|
||||
const timePart = timestamp.split('T')[1];
|
||||
if (!timePart) return '';
|
||||
return timePart.slice(0, 8); // HH:MM:SS
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// PUBLIC RENDERING FUNCTIONS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Render system prompt for SDK session initialization
|
||||
*/
|
||||
export function renderSystemPrompt(
|
||||
variables: SystemPromptVariables
|
||||
): string {
|
||||
// Auto-truncate userPrompt
|
||||
const userPromptTruncated = truncate(
|
||||
variables.userPrompt,
|
||||
HOOK_CONFIG.maxUserPromptLength
|
||||
);
|
||||
|
||||
return substituteVariables(PROMPTS.system, {
|
||||
project: variables.project,
|
||||
sessionId: variables.sessionId,
|
||||
date: variables.date,
|
||||
userPrompt: userPromptTruncated,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Render tool message for SDK processing
|
||||
*/
|
||||
export function renderToolMessage(
|
||||
variables: ToolMessageVariables
|
||||
): string {
|
||||
// Auto-truncate userPrompt and toolResponse
|
||||
const userPromptTruncated = truncate(
|
||||
variables.userPrompt,
|
||||
HOOK_CONFIG.maxUserPromptLength
|
||||
);
|
||||
|
||||
const toolResponseTruncated = truncate(
|
||||
variables.toolResponse,
|
||||
HOOK_CONFIG.maxToolResponseLength
|
||||
);
|
||||
|
||||
// Format timestamp
|
||||
const timeFormatted = formatTime(variables.timestamp);
|
||||
|
||||
return substituteVariables(PROMPTS.tool, {
|
||||
toolName: variables.toolName,
|
||||
toolResponse: toolResponseTruncated,
|
||||
userPrompt: userPromptTruncated,
|
||||
timestamp: variables.timestamp,
|
||||
timeFormatted,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Render end message for session completion
|
||||
*/
|
||||
export function renderEndMessage(
|
||||
variables: EndMessageVariables
|
||||
): string {
|
||||
return substituteVariables(PROMPTS.end, {
|
||||
project: variables.project,
|
||||
sessionId: variables.sessionId,
|
||||
});
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// GENERIC RENDERER (for convenience)
|
||||
// =============================================================================
|
||||
|
||||
export type PromptType = 'system' | 'tool' | 'end';
|
||||
|
||||
export type PromptVariables<T extends PromptType> = T extends 'system'
|
||||
? SystemPromptVariables
|
||||
: T extends 'tool'
|
||||
? ToolMessageVariables
|
||||
: T extends 'end'
|
||||
? EndMessageVariables
|
||||
: never;
|
||||
|
||||
/**
|
||||
* Generic prompt renderer - dispatches to specific renderer based on type
|
||||
*/
|
||||
export function renderPrompt<T extends PromptType>(
|
||||
type: T,
|
||||
variables: PromptVariables<T>
|
||||
): string {
|
||||
switch (type) {
|
||||
case 'system':
|
||||
return renderSystemPrompt(variables as SystemPromptVariables);
|
||||
case 'tool':
|
||||
return renderToolMessage(variables as ToolMessageVariables);
|
||||
case 'end':
|
||||
return renderEndMessage(variables as EndMessageVariables);
|
||||
default:
|
||||
throw new Error(`Unknown prompt type: ${type}`);
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// EXPORTS
|
||||
// =============================================================================
|
||||
|
||||
export { HOOK_CONFIG, PROMPTS };
|
||||
@@ -0,0 +1,306 @@
|
||||
/**
|
||||
* Hook Prompts Configuration
|
||||
*
|
||||
* Centralized configuration for all streaming hook prompts.
|
||||
* This is the SINGLE SOURCE OF TRUTH for hook prompt content.
|
||||
*
|
||||
* EDITING GUIDE:
|
||||
* - Use {{variableName}} for template variables
|
||||
* - Available variables are listed in each prompt's interface
|
||||
* - All prompts are processed through renderPrompt() function
|
||||
* - Changes here apply to all hooks automatically after rebuild
|
||||
*
|
||||
* LIFECYCLE FLOW:
|
||||
* 1. user-prompt-submit: Initializes SDK session with systemPrompt
|
||||
* 2. post-tool-use: Feeds tool responses using toolMessage (repeats N times)
|
||||
* 3. stop: Ends session and requests overview using endMessage
|
||||
*/
|
||||
|
||||
// =============================================================================
|
||||
// TYPE DEFINITIONS
|
||||
// =============================================================================
|
||||
|
||||
export interface SystemPromptVariables {
|
||||
project: string;
|
||||
sessionId: string;
|
||||
date: string;
|
||||
userPrompt: string; // Auto-truncated to maxUserPromptLength
|
||||
}
|
||||
|
||||
export interface ToolMessageVariables {
|
||||
toolName: string;
|
||||
toolResponse: string; // Auto-truncated to maxToolResponseLength
|
||||
userPrompt: string; // Auto-truncated to maxUserPromptLength
|
||||
timestamp: string; // Full ISO timestamp
|
||||
timeFormatted: string; // HH:MM:SS format
|
||||
}
|
||||
|
||||
export interface EndMessageVariables {
|
||||
project: string;
|
||||
sessionId: string;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// SHARED CONFIGURATION
|
||||
// =============================================================================
|
||||
|
||||
export const HOOK_CONFIG = {
|
||||
// Truncation limits for template variables
|
||||
maxUserPromptLength: 200,
|
||||
maxToolResponseLength: 20000,
|
||||
|
||||
// SDK configuration (used by hooks)
|
||||
sdk: {
|
||||
model: 'claude-sonnet-4-5',
|
||||
allowedTools: ['Bash'],
|
||||
maxTokensSystem: 8192,
|
||||
maxTokensTool: 8192,
|
||||
maxTokensEnd: 2048,
|
||||
},
|
||||
} as const;
|
||||
|
||||
// =============================================================================
|
||||
// PHASE 1: SYSTEM PROMPT (user-prompt-submit-streaming.js)
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* System prompt that initializes the SDK session.
|
||||
* Instructs the SDK on how to process tool responses and store memories.
|
||||
*
|
||||
* Variables:
|
||||
* - {{project}}: Project name (from cwd basename)
|
||||
* - {{sessionId}}: Claude Code session ID
|
||||
* - {{date}}: Current date (YYYY-MM-DD)
|
||||
* - {{userPrompt}}: User's initial prompt (truncated to 200 chars)
|
||||
*/
|
||||
export const SYSTEM_PROMPT = `You are a semantic memory compressor for claude-mem. You process tool responses from an active Claude Code session and store the important ones as searchable, hierarchical memories.
|
||||
|
||||
# SESSION CONTEXT
|
||||
- Project: {{project}}
|
||||
- Session: {{sessionId}}
|
||||
- Date: {{date}}
|
||||
- User Request: "{{userPrompt}}"
|
||||
|
||||
# YOUR JOB
|
||||
|
||||
## FIRST: Generate Session Title
|
||||
|
||||
IMMEDIATELY generate a title and subtitle for this session based on the user request.
|
||||
|
||||
Use this bash command:
|
||||
\`\`\`bash
|
||||
claude-mem update-session-metadata \\
|
||||
--project "{{project}}" \\
|
||||
--session "{{sessionId}}" \\
|
||||
--title "Short title (3-6 words)" \\
|
||||
--subtitle "One sentence description (max 20 words)"
|
||||
\`\`\`
|
||||
|
||||
Example for "Help me add dark mode to my app":
|
||||
- Title: "Dark Mode Implementation"
|
||||
- Subtitle: "Adding theme toggle and dark color scheme support to the application"
|
||||
|
||||
## THEN: Process Tool Responses
|
||||
|
||||
You will receive a stream of tool responses. For each one:
|
||||
|
||||
1. ANALYZE: Does this contain information worth remembering?
|
||||
2. DECIDE: Should I store this or skip it?
|
||||
3. EXTRACT: What are the key semantic concepts?
|
||||
4. DECOMPOSE: Break into title + subtitle + atomic facts + narrative
|
||||
5. STORE: Use bash to save the hierarchical memory
|
||||
6. TRACK: Keep count of stored memories (001, 002, 003...)
|
||||
|
||||
# WHAT TO STORE
|
||||
|
||||
Store these:
|
||||
- File contents with logic, algorithms, or patterns
|
||||
- Search results revealing project structure
|
||||
- Build errors or test failures with context
|
||||
- Code revealing architecture or design decisions
|
||||
- Git diffs with significant changes
|
||||
- Command outputs showing system state
|
||||
|
||||
Skip these:
|
||||
- Simple status checks (git status with no changes)
|
||||
- Trivial edits (one-line config changes)
|
||||
- Repeated operations
|
||||
- Binary data or noise
|
||||
- Anything without semantic value
|
||||
|
||||
# HIERARCHICAL MEMORY FORMAT
|
||||
|
||||
Each memory has FOUR components:
|
||||
|
||||
## 1. TITLE (3-8 words)
|
||||
A scannable headline that captures the core action or topic.
|
||||
Examples:
|
||||
- "SDK Transcript Cleanup Implementation"
|
||||
- "Hook System Architecture Analysis"
|
||||
- "ChromaDB Migration Planning"
|
||||
|
||||
## 2. SUBTITLE (max 24 words)
|
||||
A concise, memorable summary that captures the essence of the change.
|
||||
Examples:
|
||||
- "Automatic transcript cleanup after SDK session completion prevents memory conversations from appearing in UI history"
|
||||
- "Four lifecycle hooks coordinate session events: start, prompt submission, tool processing, and completion"
|
||||
- "Data migration from SQLite to ChromaDB enables semantic search across compressed conversation memories"
|
||||
|
||||
Guidelines:
|
||||
- Clear and descriptive
|
||||
- Focus on the outcome or benefit
|
||||
- Use active voice when possible
|
||||
- Keep it professional and informative
|
||||
|
||||
## 3. ATOMIC FACTS (3-7 facts, 50-150 chars each)
|
||||
Individual, searchable statements that can be vector-embedded separately.
|
||||
Each fact is ONE specific piece of information.
|
||||
|
||||
Examples:
|
||||
- "stop-streaming.js: Auto-deletes SDK transcripts after completion"
|
||||
- "Path format: ~/.claude/projects/{sanitized-cwd}/{sessionId}.jsonl"
|
||||
- "Uses fs.unlink with graceful error handling for missing files"
|
||||
- "Checks two transcript path formats for backward compatibility"
|
||||
|
||||
Guidelines:
|
||||
- Start with filename or component when relevant
|
||||
- Be specific: include paths, function names, actual values
|
||||
- Each fact stands alone (no pronouns like "it" or "this")
|
||||
- 50-150 characters target
|
||||
- Focus on searchable technical details
|
||||
|
||||
## 4. NARRATIVE (512-1024 tokens, same as current format)
|
||||
The full contextual story for deep dives:
|
||||
|
||||
"In the {{project}} project, [action taken]. [Technical details: files, functions, concepts]. [Why this matters]."
|
||||
|
||||
This is the detailed explanation for when someone needs full context.
|
||||
|
||||
# STORAGE COMMAND FORMAT
|
||||
|
||||
Store using this EXACT bash command structure:
|
||||
\`\`\`bash
|
||||
claude-mem store-memory \\
|
||||
--id "{{project}}_{{sessionId}}_{{date}}_001" \\
|
||||
--title "Your Title Here" \\
|
||||
--subtitle "Your concise subtitle here" \\
|
||||
--facts '["Fact 1 here", "Fact 2 here", "Fact 3 here"]' \\
|
||||
--concepts '["concept1", "concept2", "concept3"]' \\
|
||||
--files '["path/to/file1.js", "path/to/file2.ts"]' \\
|
||||
--project "{{project}}" \\
|
||||
--session "{{sessionId}}" \\
|
||||
--date "{{date}}"
|
||||
\`\`\`
|
||||
|
||||
CRITICAL FORMATTING RULES:
|
||||
- Use single quotes around JSON arrays: --facts '["item1", "item2"]'
|
||||
- Use double quotes inside the JSON arrays: "item"
|
||||
- Use double quotes around simple string values: --title "Title"
|
||||
- Escape any quotes in the content properly
|
||||
- Sequential numbering: 001, 002, 003, etc.
|
||||
|
||||
Concepts: 2-5 broad categories (e.g., "hooks", "storage", "async-processing")
|
||||
Files: Actual file paths touched (e.g., "hooks/stop-streaming.js")
|
||||
|
||||
# EXAMPLE MEMORY
|
||||
|
||||
Tool response shows: [Read file hooks/stop-streaming.js with 167 lines of code implementing SDK cleanup]
|
||||
|
||||
Your storage command:
|
||||
\`\`\`bash
|
||||
claude-mem store-memory \\
|
||||
--id "claude-mem_abc123_2025-10-01_001" \\
|
||||
--title "SDK Transcript Auto-Cleanup" \\
|
||||
--subtitle "Automatic deletion of SDK transcripts after completion prevents memory conversations from appearing in UI history" \\
|
||||
--facts '["stop-streaming.js: Deletes SDK transcript after overview generation", "Path: ~/.claude/projects/{sanitized-cwd}/{sessionId}.jsonl", "Uses fs.unlink with error handling for missing files", "Prevents memory conversations from polluting Claude Code UI"]' \\
|
||||
--concepts '["cleanup", "SDK-lifecycle", "UX", "file-management"]' \\
|
||||
--files '["hooks/stop-streaming.js"]' \\
|
||||
--project "claude-mem" \\
|
||||
--session "abc123" \\
|
||||
--date "2025-10-01"
|
||||
\`\`\`
|
||||
|
||||
# STATE TRACKING
|
||||
|
||||
CRITICAL: Keep track of your memory counter across all tool messages.
|
||||
- Start at 001
|
||||
- Increment for each stored memory
|
||||
- Never repeat numbers
|
||||
- Each session has separate numbering
|
||||
|
||||
# SESSION END
|
||||
|
||||
At the end (when I send "SESSION ENDING"), generate an overview using:
|
||||
\`\`\`bash
|
||||
claude-mem store-overview --project "{{project}}" --session "{{sessionId}}" --content "2-3 sentence overview"
|
||||
\`\`\`
|
||||
|
||||
# IMPORTANT REMINDERS
|
||||
|
||||
- You're processing a DIFFERENT Claude Code session (not your own)
|
||||
- Use Bash tool to call claude-mem commands
|
||||
- Keep subtitles clear and informative (max 24 words)
|
||||
- Each fact is ONE specific thing (not multiple ideas)
|
||||
- Be selective - quality over quantity
|
||||
- Always increment memory numbers
|
||||
- Facts should be searchable (specific file names, paths, functions)
|
||||
|
||||
Ready for tool responses.`;
|
||||
|
||||
// =============================================================================
|
||||
// PHASE 2: TOOL MESSAGE (post-tool-use-streaming.js)
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Message format for each tool response sent to the SDK.
|
||||
* The SDK analyzes this and decides whether to store a memory.
|
||||
*
|
||||
* Variables:
|
||||
* - {{timeFormatted}}: Time portion of timestamp (HH:MM:SS)
|
||||
* - {{toolName}}: Name of the tool that was used
|
||||
* - {{userPrompt}}: User's original prompt (truncated to 200 chars)
|
||||
* - {{toolResponse}}: Full tool response (truncated to 20000 chars)
|
||||
*/
|
||||
export const TOOL_MESSAGE = `# Tool Response {{timeFormatted}}
|
||||
|
||||
Tool: {{toolName}}
|
||||
User Context: "{{userPrompt}}"
|
||||
|
||||
\`\`\`
|
||||
{{toolResponse}}
|
||||
\`\`\`
|
||||
|
||||
Analyze and store if meaningful.`;
|
||||
|
||||
// =============================================================================
|
||||
// PHASE 3: END MESSAGE (stop-streaming.js)
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Message sent to SDK when session ends.
|
||||
* Requests the SDK to generate and store a session overview.
|
||||
*
|
||||
* Variables:
|
||||
* - {{project}}: Project name
|
||||
* - {{sessionId}}: Claude Code session ID
|
||||
*/
|
||||
export const END_MESSAGE = `# SESSION ENDING
|
||||
|
||||
Review our entire conversation. Generate a concise 2-3 sentence overview of what was accomplished.
|
||||
|
||||
Store it using Bash:
|
||||
\`\`\`bash
|
||||
claude-mem store-overview --project "{{project}}" --session "{{sessionId}}" --content "YOUR_OVERVIEW_HERE"
|
||||
\`\`\`
|
||||
|
||||
Focus on: what was done, current state, key decisions, outcomes.`;
|
||||
|
||||
// =============================================================================
|
||||
// EXPORTS
|
||||
// =============================================================================
|
||||
|
||||
export const PROMPTS = {
|
||||
system: SYSTEM_PROMPT,
|
||||
tool: TOOL_MESSAGE,
|
||||
end: END_MESSAGE,
|
||||
} as const;
|
||||
@@ -0,0 +1,491 @@
|
||||
/**
|
||||
* Context Templates for Human-Readable Formatting
|
||||
*
|
||||
* Essential templates for user-facing messages in the memory system.
|
||||
* Focused on session start messages, error handling, and operation feedback.
|
||||
* Previously included Handlebars templates for session start formatting; current
|
||||
* version renders directly via console for clarity and performance.
|
||||
*/
|
||||
|
||||
import { formatRelativeTime, parseTimestamp } from '../../../lib/time-utils.js';
|
||||
|
||||
// =============================================================================
|
||||
// TERMINAL WIDTH & WORD WRAPPING
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Determines target wrap width based on:
|
||||
* 1) CLAUDE_MEM_WRAP_WIDTH env override
|
||||
* 2) TTY columns (capped at 120)
|
||||
* 3) Fallback default of 80
|
||||
*/
|
||||
function getWrapWidth(): number {
|
||||
const env = process.env.CLAUDE_MEM_WRAP_WIDTH;
|
||||
if (env) {
|
||||
const n = parseInt(env, 10);
|
||||
if (!Number.isNaN(n) && n > 40 && n <= 200) return n;
|
||||
}
|
||||
// Default to classic 80 columns unless overridden
|
||||
return 80;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap a single logical line to the given width, preserving leading indentation.
|
||||
* Also avoids wrapping pure separator lines (====, ----, etc.).
|
||||
*/
|
||||
function wrapSingleLine(line: string, width: number): string {
|
||||
if (!line) return '';
|
||||
// Don't wrap long separator lines
|
||||
if (/^[\-=\u2014_\u2500\u2550]{5,}$/.test(line.trim())) return line;
|
||||
|
||||
// If already short enough, return as-is
|
||||
if (line.length <= width) return line;
|
||||
|
||||
const indentMatch = line.match(/^\s*/);
|
||||
const indent = indentMatch ? indentMatch[0] : '';
|
||||
const content = line.slice(indent.length);
|
||||
const avail = Math.max(10, width - indent.length); // keep some minimum
|
||||
|
||||
const words = content.split(/(\s+)/); // keep whitespace tokens
|
||||
const out: string[] = [];
|
||||
let current = '';
|
||||
|
||||
const pushLine = () => {
|
||||
out.push(indent + current.trimEnd());
|
||||
current = '';
|
||||
};
|
||||
|
||||
for (const token of words) {
|
||||
if (token === '') continue;
|
||||
// If token itself is longer than available width, hard-break it
|
||||
if (!/\s/.test(token) && token.length > avail) {
|
||||
if (current.trim().length > 0) pushLine();
|
||||
let start = 0;
|
||||
while (start < token.length) {
|
||||
const chunk = token.slice(start, start + avail);
|
||||
out.push(indent + chunk);
|
||||
start += avail;
|
||||
}
|
||||
current = '';
|
||||
continue;
|
||||
}
|
||||
|
||||
if (indent.length + current.length + token.length > width) {
|
||||
pushLine();
|
||||
}
|
||||
current += token;
|
||||
}
|
||||
|
||||
if (current.trim().length > 0 || out.length === 0) pushLine();
|
||||
return out.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap a block of text (possibly multi-line) to the given width.
|
||||
* Preserves blank lines and wraps each line independently.
|
||||
*/
|
||||
function wrapText(text: string, width: number): string {
|
||||
if (!text) return '';
|
||||
return text
|
||||
.split('\n')
|
||||
.map((line) => wrapSingleLine(line, width))
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
/** Create a full-width horizontal line with the given character */
|
||||
function makeLine(char: string = '─', width: number = getWrapWidth()): string {
|
||||
if (!char || char.length === 0) char = '-';
|
||||
// Repeat and slice to exact width to avoid multi-column surprises
|
||||
return char.repeat(width).slice(0, width);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// SESSION START MESSAGES
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Creates a completion message after context operations
|
||||
*/
|
||||
export function createCompletionMessage(
|
||||
operation: string,
|
||||
count?: number,
|
||||
details?: string
|
||||
): string {
|
||||
const countInfo = count !== undefined ? ` (${count} items)` : '';
|
||||
const detailInfo = details ? `\n${details}` : '';
|
||||
const width = getWrapWidth();
|
||||
return wrapText(
|
||||
`✅ ${operation} completed successfully${countInfo}${detailInfo}`,
|
||||
width
|
||||
);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// ERROR MESSAGES (USER-FRIENDLY)
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Creates user-friendly error messages with helpful suggestions
|
||||
*/
|
||||
export function createUserFriendlyError(
|
||||
operation: string,
|
||||
error: string,
|
||||
suggestion?: string
|
||||
): string {
|
||||
const suggestionText = suggestion ? `\n\n💡 ${suggestion}` : '';
|
||||
const width = getWrapWidth();
|
||||
return wrapText(
|
||||
`❌ ${operation} encountered an issue: ${error}${suggestionText}`,
|
||||
width
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Common error scenarios with built-in suggestions
|
||||
*/
|
||||
export const ERROR_SCENARIOS = {
|
||||
NO_MEMORIES: (projectName: string) => ({
|
||||
message: `No previous memories found for ${projectName}`,
|
||||
suggestion:
|
||||
'This appears to be your first session. Memories will be created as you work.',
|
||||
}),
|
||||
|
||||
CONNECTION_FAILED: () => ({
|
||||
message: 'Could not connect to memory system',
|
||||
suggestion:
|
||||
'Try restarting Claude Code or check if the MCP server is properly configured.',
|
||||
}),
|
||||
|
||||
SEARCH_FAILED: (query: string) => ({
|
||||
message: `Search for "${query}" didn't return any results`,
|
||||
suggestion:
|
||||
'Try using different keywords or check if memories exist for this project.',
|
||||
}),
|
||||
|
||||
LOAD_TIMEOUT: () => ({
|
||||
message: 'Memory loading timed out',
|
||||
suggestion:
|
||||
'The operation is taking longer than expected. You can continue without loaded context.',
|
||||
}),
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates contextual error messages based on common scenarios
|
||||
*/
|
||||
export function createContextualError(
|
||||
scenario: keyof typeof ERROR_SCENARIOS,
|
||||
...args: string[]
|
||||
): string {
|
||||
const errorInfo = (ERROR_SCENARIOS[scenario] as any)(...args);
|
||||
return createUserFriendlyError(
|
||||
'Memory system',
|
||||
errorInfo.message,
|
||||
errorInfo.suggestion
|
||||
);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// TIME AND DATE FORMATTING
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Formats timestamps into human-readable "time ago" format
|
||||
*/
|
||||
export function formatTimeAgo(timestamp: string | Date): string {
|
||||
const date = typeof timestamp === 'string' ? new Date(timestamp) : timestamp;
|
||||
const now = new Date();
|
||||
const diff = now.getTime() - date.getTime();
|
||||
|
||||
const minutes = Math.floor(diff / 60000);
|
||||
const hours = Math.floor(diff / 3600000);
|
||||
const days = Math.floor(diff / 86400000);
|
||||
|
||||
if (minutes < 1) return 'just now';
|
||||
if (minutes < 60) return `${minutes} minute${minutes > 1 ? 's' : ''} ago`;
|
||||
if (hours < 24) return `${hours} hour${hours > 1 ? 's' : ''} ago`;
|
||||
if (days < 7) return `${days} day${days > 1 ? 's' : ''} ago`;
|
||||
|
||||
return date.toLocaleDateString();
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// SESSION START TEMPLATE SYSTEM (data processing only)
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Interface for memory entry data structure
|
||||
*/
|
||||
interface MemoryEntry {
|
||||
summary: string;
|
||||
keywords?: string;
|
||||
location?: string;
|
||||
sessionId?: string;
|
||||
number?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for grouped session memories
|
||||
*/
|
||||
interface SessionGroup {
|
||||
sessionId: string;
|
||||
memories: MemoryEntry[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for overview with timestamp
|
||||
*/
|
||||
interface OverviewEntry {
|
||||
content: string;
|
||||
timestamp?: Date;
|
||||
timeAgo?: string;
|
||||
sessionId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for session-grouped overviews
|
||||
*/
|
||||
interface SessionOverviewGroup {
|
||||
sessionId: string;
|
||||
overviews: OverviewEntry[];
|
||||
earliestTimestamp?: Date;
|
||||
timeAgo?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pure data processing function - converts JSON objects into structured memory entries
|
||||
* No formatting is done here, only data parsing and cleaning
|
||||
*/
|
||||
function processMemoryEntries(recentObjects: any[]): MemoryEntry[] {
|
||||
if (recentObjects.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Filter only memory type objects and convert to MemoryEntry format
|
||||
return recentObjects
|
||||
.filter((obj) => obj.type === 'memory')
|
||||
.map((obj) => {
|
||||
const entry: MemoryEntry = {
|
||||
summary: obj.text || '',
|
||||
sessionId: obj.session_id || '',
|
||||
};
|
||||
|
||||
// Add optional fields if present
|
||||
if (obj.keywords) {
|
||||
entry.keywords = obj.keywords;
|
||||
}
|
||||
if (obj.document_id && !obj.document_id.includes('Session:')) {
|
||||
entry.location = obj.document_id;
|
||||
}
|
||||
|
||||
return entry;
|
||||
})
|
||||
.filter((entry) => entry.summary.length > 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Groups memories by session ID and adds numbering
|
||||
*/
|
||||
function groupMemoriesBySession(memories: MemoryEntry[]): SessionGroup[] {
|
||||
const sessionMap = new Map<string, MemoryEntry[]>();
|
||||
|
||||
// Group memories by session ID
|
||||
memories.forEach((memory) => {
|
||||
const sessionId = memory.sessionId;
|
||||
if (sessionId) {
|
||||
if (!sessionMap.has(sessionId)) {
|
||||
sessionMap.set(sessionId, []);
|
||||
}
|
||||
sessionMap.get(sessionId)!.push(memory);
|
||||
}
|
||||
});
|
||||
|
||||
// Convert to session groups with numbering
|
||||
return Array.from(sessionMap.entries()).map(
|
||||
([sessionId, sessionMemories]) => {
|
||||
const numberedMemories = sessionMemories.map((memory, index) => ({
|
||||
...memory,
|
||||
number: index + 1,
|
||||
}));
|
||||
|
||||
return {
|
||||
sessionId,
|
||||
memories: numberedMemories,
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Groups overviews by session ID and calculates session timestamps
|
||||
*/
|
||||
function groupOverviewsBySession(
|
||||
overviews: OverviewEntry[]
|
||||
): SessionOverviewGroup[] {
|
||||
const sessionMap = new Map<string, OverviewEntry[]>();
|
||||
|
||||
// Group overviews by session ID
|
||||
overviews.forEach((overview) => {
|
||||
const sessionId = overview.sessionId || 'unknown';
|
||||
if (!sessionMap.has(sessionId)) {
|
||||
sessionMap.set(sessionId, []);
|
||||
}
|
||||
sessionMap.get(sessionId)!.push(overview);
|
||||
});
|
||||
|
||||
// Convert to session groups with timestamps
|
||||
return Array.from(sessionMap.entries()).map(
|
||||
([sessionId, sessionOverviews]) => {
|
||||
// Find the earliest timestamp in this session's overviews
|
||||
const timestamps = sessionOverviews
|
||||
.map((o) => o.timestamp)
|
||||
.filter((t): t is Date => t !== undefined)
|
||||
.sort((a, b) => a.getTime() - b.getTime());
|
||||
|
||||
const group: SessionOverviewGroup = {
|
||||
sessionId,
|
||||
overviews: sessionOverviews,
|
||||
};
|
||||
|
||||
// Add session-level timestamp if available
|
||||
if (timestamps.length > 0) {
|
||||
group.earliestTimestamp = timestamps[0];
|
||||
group.timeAgo = formatRelativeTime(timestamps[0]);
|
||||
}
|
||||
|
||||
return group;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the complete session start template with provided data using Handlebars
|
||||
* Data processing is separated from presentation - template controls the format
|
||||
*/
|
||||
// Intentionally removed Handlebars-based renderer; console output is handled by
|
||||
// outputSessionStartContent() below.
|
||||
|
||||
/**
|
||||
* Outputs session start content using dual streams:
|
||||
* - stdout (console.log) -> Claude's context only (granular memories)
|
||||
* - stderr (console.error) -> User visible (clean overviews)
|
||||
*/
|
||||
export function outputSessionStartContent(params: {
|
||||
projectName: string;
|
||||
memoryCount: number;
|
||||
lastSessionTime?: string;
|
||||
recentObjects: any[];
|
||||
}): void {
|
||||
const { projectName, memoryCount, lastSessionTime, recentObjects } = params;
|
||||
const width = getWrapWidth();
|
||||
|
||||
// Start with current date and time at the top
|
||||
const now = new Date();
|
||||
const dateTimeFormatted = now.toLocaleString('en-US', {
|
||||
weekday: 'long',
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
timeZoneName: 'short',
|
||||
});
|
||||
|
||||
console.log('');
|
||||
console.log(wrapText(`📅 ${dateTimeFormatted}`, width));
|
||||
console.log(makeLine('─', width));
|
||||
|
||||
// Extract overviews for user display - get more to show session grouping
|
||||
const overviews = extractOverviews(recentObjects, 10, projectName);
|
||||
|
||||
// Debug: Log what we're getting
|
||||
console.error(`[DEBUG] recentObjects has ${recentObjects.length} items`);
|
||||
console.error(`[DEBUG] overviews extracted: ${overviews.length}`);
|
||||
|
||||
// Process memory entries for Claude context
|
||||
const memories = processMemoryEntries(recentObjects);
|
||||
// Helper to split and normalize keywords into a map (lowercased -> original)
|
||||
const splitKeywordsInto = (kw: string, dest: Map<string, string>) => {
|
||||
const tokens =
|
||||
kw.includes(',') || kw.includes('\n') ? kw.split(/[\n,]+/) : [kw];
|
||||
for (const t of tokens) {
|
||||
const trimmed = t.trim();
|
||||
if (!trimmed) continue;
|
||||
const key = trimmed.toLowerCase();
|
||||
if (!dest.has(key)) dest.set(key, trimmed);
|
||||
}
|
||||
};
|
||||
|
||||
// Output memories first, then overviews at bottom, all sorted oldest to newest
|
||||
if (memories.length > 0) {
|
||||
const sessionGroups = groupMemoriesBySession(memories);
|
||||
console.log('');
|
||||
console.log('');
|
||||
|
||||
console.log(wrapText('📚 Memories', width));
|
||||
sessionGroups.forEach((group) => {
|
||||
console.log(makeLine('─', width));
|
||||
|
||||
console.log('');
|
||||
|
||||
console.log(wrapText(`🔍 ${group.sessionId}`, width));
|
||||
|
||||
// Collect keywords for this session as we iterate its memories
|
||||
const groupKeywordMap = new Map<string, string>();
|
||||
|
||||
group.memories.forEach((memory) => {
|
||||
console.log('');
|
||||
console.log(wrapText(`${memory.number}. ${memory.summary}`, width));
|
||||
if (memory.keywords)
|
||||
splitKeywordsInto(memory.keywords, groupKeywordMap);
|
||||
});
|
||||
|
||||
// Print this session's aggregated keywords under the session block
|
||||
const groupKeywords = Array.from(groupKeywordMap.values());
|
||||
if (groupKeywords.length > 0) {
|
||||
console.log('');
|
||||
console.log(wrapText(`🏷️ ${groupKeywords.join(', ')}`, width));
|
||||
}
|
||||
console.log('');
|
||||
});
|
||||
}
|
||||
|
||||
// Overview section at bottom with session grouping
|
||||
if (overviews.length > 0) {
|
||||
const sessionGroups = groupOverviewsBySession(overviews);
|
||||
|
||||
// Sort groups by timestamp, oldest first for chronological reading order
|
||||
sessionGroups.sort((a, b) => {
|
||||
const timeA = a.earliestTimestamp?.getTime() || 0;
|
||||
const timeB = b.earliestTimestamp?.getTime() || 0;
|
||||
return timeA - timeB; // Ascending order (oldest first)
|
||||
});
|
||||
|
||||
console.log('');
|
||||
|
||||
console.log(wrapText('🧠 Overviews', width));
|
||||
console.log(makeLine('─', width));
|
||||
|
||||
// Match the memories section layout: session header, numbered items, per-session separator
|
||||
sessionGroups.forEach((group) => {
|
||||
console.log('');
|
||||
console.log(wrapText(`🔍 ${group.sessionId}`, width));
|
||||
|
||||
group.overviews.forEach((overview, index) => {
|
||||
console.log('');
|
||||
console.log(wrapText(`${index + 1}. ${overview.content}`, width));
|
||||
console.log('');
|
||||
|
||||
if (overview.timeAgo) {
|
||||
console.log(wrapText(`📅 ${overview.timeAgo}`, width));
|
||||
}
|
||||
});
|
||||
|
||||
console.log('');
|
||||
console.log(makeLine('─', width));
|
||||
});
|
||||
} else if (memories.length === 0) {
|
||||
console.log(
|
||||
wrapText(`🧠 No recent context found for ${projectName}`, width)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,373 @@
|
||||
import { join, dirname, sep } from 'path';
|
||||
import { homedir } from 'os';
|
||||
import { existsSync, statSync } from 'fs';
|
||||
import { execSync } from 'child_process';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
/**
|
||||
* PathDiscovery Service - Central path resolution for claude-mem
|
||||
*
|
||||
* Handles dynamic discovery of all required paths across different installation scenarios:
|
||||
* - npm global installs, local installs, and development environments
|
||||
* - Cross-platform path resolution (Windows, macOS, Linux)
|
||||
* - Environment variable overrides for customization
|
||||
* - Package resource discovery (hooks, commands)
|
||||
*/
|
||||
export class PathDiscovery {
|
||||
private static instance: PathDiscovery | null = null;
|
||||
|
||||
// Cached paths for performance
|
||||
private _dataDirectory: string | null = null;
|
||||
private _packageRoot: string | null = null;
|
||||
private _claudeConfigDirectory: string | null = null;
|
||||
|
||||
/**
|
||||
* Get singleton instance
|
||||
*/
|
||||
static getInstance(): PathDiscovery {
|
||||
if (!PathDiscovery.instance) {
|
||||
PathDiscovery.instance = new PathDiscovery();
|
||||
}
|
||||
return PathDiscovery.instance;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// DATA DIRECTORIES - Where claude-mem stores its data
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Base data directory for claude-mem
|
||||
* Environment override: CLAUDE_MEM_DATA_DIR
|
||||
*/
|
||||
getDataDirectory(): string {
|
||||
if (this._dataDirectory) return this._dataDirectory;
|
||||
|
||||
this._dataDirectory = process.env.CLAUDE_MEM_DATA_DIR || join(homedir(), '.claude-mem');
|
||||
return this._dataDirectory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Archives directory for compressed sessions
|
||||
*/
|
||||
getArchivesDirectory(): string {
|
||||
return join(this.getDataDirectory(), 'archives');
|
||||
}
|
||||
|
||||
/**
|
||||
* Hooks directory where claude-mem hooks are installed
|
||||
*/
|
||||
getHooksDirectory(): string {
|
||||
return join(this.getDataDirectory(), 'hooks');
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs directory for claude-mem operation logs
|
||||
*/
|
||||
getLogsDirectory(): string {
|
||||
return join(this.getDataDirectory(), 'logs');
|
||||
}
|
||||
|
||||
/**
|
||||
* Index directory for memory indexing
|
||||
*/
|
||||
getIndexDirectory(): string {
|
||||
return this.getDataDirectory();
|
||||
}
|
||||
|
||||
/**
|
||||
* Index file path for memory indexing
|
||||
*/
|
||||
getIndexPath(): string {
|
||||
return join(this.getIndexDirectory(), 'claude-mem-index.jsonl');
|
||||
}
|
||||
|
||||
/**
|
||||
* Trash directory for smart trash feature
|
||||
*/
|
||||
getTrashDirectory(): string {
|
||||
return join(this.getDataDirectory(), 'trash');
|
||||
}
|
||||
|
||||
/**
|
||||
* Backups directory for configuration backups
|
||||
*/
|
||||
getBackupsDirectory(): string {
|
||||
return join(this.getDataDirectory(), 'backups');
|
||||
}
|
||||
|
||||
/**
|
||||
* Chroma database directory
|
||||
*/
|
||||
getChromaDirectory(): string {
|
||||
return join(this.getDataDirectory(), 'chroma');
|
||||
}
|
||||
|
||||
/**
|
||||
* Project-specific archive directory
|
||||
*/
|
||||
getProjectArchiveDirectory(projectName: string): string {
|
||||
return join(this.getArchivesDirectory(), projectName);
|
||||
}
|
||||
|
||||
/**
|
||||
* User settings file path
|
||||
*/
|
||||
getUserSettingsPath(): string {
|
||||
return join(this.getDataDirectory(), 'settings.json');
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// CLAUDE INTEGRATION PATHS - Where Claude Code expects configuration
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Claude configuration directory
|
||||
* Environment override: CLAUDE_CONFIG_DIR
|
||||
*/
|
||||
getClaudeConfigDirectory(): string {
|
||||
if (this._claudeConfigDirectory) return this._claudeConfigDirectory;
|
||||
|
||||
this._claudeConfigDirectory = process.env.CLAUDE_CONFIG_DIR || join(homedir(), '.claude');
|
||||
return this._claudeConfigDirectory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Claude settings file path
|
||||
*/
|
||||
getClaudeSettingsPath(): string {
|
||||
return join(this.getClaudeConfigDirectory(), 'settings.json');
|
||||
}
|
||||
|
||||
/**
|
||||
* Claude commands directory where custom commands are installed
|
||||
*/
|
||||
getClaudeCommandsDirectory(): string {
|
||||
return join(this.getClaudeConfigDirectory(), 'commands');
|
||||
}
|
||||
|
||||
/**
|
||||
* CLAUDE.md instructions file path
|
||||
*/
|
||||
getClaudeMdPath(): string {
|
||||
return join(this.getClaudeConfigDirectory(), 'CLAUDE.md');
|
||||
}
|
||||
|
||||
/**
|
||||
* MCP configuration file path (user-level)
|
||||
*/
|
||||
getMcpConfigPath(): string {
|
||||
return join(homedir(), '.claude.json');
|
||||
}
|
||||
|
||||
/**
|
||||
* MCP configuration file path (project-level)
|
||||
*/
|
||||
getProjectMcpConfigPath(): string {
|
||||
return join(process.cwd(), '.mcp.json');
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// PACKAGE DISCOVERY - Find claude-mem package resources
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Discover the claude-mem package root directory
|
||||
*/
|
||||
getPackageRoot(): string {
|
||||
if (this._packageRoot) return this._packageRoot;
|
||||
|
||||
// Method 1: Try require.resolve for package.json
|
||||
try {
|
||||
const packageJsonPath = require.resolve('claude-mem/package.json');
|
||||
this._packageRoot = dirname(packageJsonPath);
|
||||
return this._packageRoot;
|
||||
} catch {
|
||||
// Continue to next method
|
||||
}
|
||||
|
||||
// Method 2: Walk up from current module location
|
||||
const currentFile = fileURLToPath(import.meta.url);
|
||||
let currentDir = dirname(currentFile);
|
||||
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const packageJsonPath = join(currentDir, 'package.json');
|
||||
if (existsSync(packageJsonPath)) {
|
||||
const packageJson = require(packageJsonPath);
|
||||
if (packageJson.name === 'claude-mem') {
|
||||
this._packageRoot = currentDir;
|
||||
return this._packageRoot;
|
||||
}
|
||||
}
|
||||
|
||||
const parentDir = dirname(currentDir);
|
||||
if (parentDir === currentDir) break;
|
||||
currentDir = parentDir;
|
||||
}
|
||||
|
||||
// Method 3: Try npm list command
|
||||
try {
|
||||
const npmOutput = execSync('npm list -g claude-mem --json 2>/dev/null || npm list claude-mem --json 2>/dev/null', {
|
||||
encoding: 'utf8'
|
||||
});
|
||||
const npmData = JSON.parse(npmOutput);
|
||||
|
||||
if (npmData.dependencies?.['claude-mem']?.resolved) {
|
||||
this._packageRoot = dirname(npmData.dependencies['claude-mem'].resolved);
|
||||
return this._packageRoot;
|
||||
}
|
||||
} catch {
|
||||
// Continue to error
|
||||
}
|
||||
|
||||
throw new Error('Cannot locate claude-mem package root. Ensure claude-mem is properly installed.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Find hook templates directory in the installed package
|
||||
*
|
||||
* This returns the SOURCE templates directory that gets copied during installation
|
||||
* to the runtime hooks directory (~/.claude-mem/hooks/)
|
||||
*/
|
||||
findPackageHookTemplatesDirectory(): string {
|
||||
const packageRoot = this.getPackageRoot();
|
||||
const hookTemplatesDir = join(packageRoot, 'hook-templates');
|
||||
|
||||
// Verify it contains expected hook template files
|
||||
const requiredHookTemplates = [
|
||||
'session-start.js',
|
||||
'stop.js',
|
||||
'user-prompt-submit.js',
|
||||
'post-tool-use.js'
|
||||
];
|
||||
for (const hookTemplateFile of requiredHookTemplates) {
|
||||
if (!existsSync(join(hookTemplatesDir, hookTemplateFile))) {
|
||||
throw new Error(`Package hook-templates directory missing required template file: ${hookTemplateFile}`);
|
||||
}
|
||||
}
|
||||
|
||||
return hookTemplatesDir;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find commands directory in the installed package
|
||||
*/
|
||||
findPackageCommandsDirectory(): string {
|
||||
const packageRoot = this.getPackageRoot();
|
||||
const commandsDir = join(packageRoot, 'commands');
|
||||
|
||||
// Verify it contains expected command files
|
||||
const requiredCommands = ['save.md'];
|
||||
for (const commandFile of requiredCommands) {
|
||||
if (!existsSync(join(commandsDir, commandFile))) {
|
||||
throw new Error(`Package commands directory missing required file: ${commandFile}`);
|
||||
}
|
||||
}
|
||||
|
||||
return commandsDir;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// UTILITY METHODS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Ensure a directory exists, creating it if necessary
|
||||
*/
|
||||
ensureDirectory(dirPath: string): void {
|
||||
if (!existsSync(dirPath)) {
|
||||
require('fs').mkdirSync(dirPath, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure multiple directories exist
|
||||
*/
|
||||
ensureDirectories(dirPaths: string[]): void {
|
||||
dirPaths.forEach(dirPath => this.ensureDirectory(dirPath));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create all claude-mem data directories
|
||||
*/
|
||||
ensureAllDataDirectories(): void {
|
||||
this.ensureDirectories([
|
||||
this.getDataDirectory(),
|
||||
this.getArchivesDirectory(),
|
||||
this.getHooksDirectory(),
|
||||
this.getLogsDirectory(),
|
||||
this.getTrashDirectory(),
|
||||
this.getBackupsDirectory(),
|
||||
this.getChromaDirectory()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create all Claude integration directories
|
||||
*/
|
||||
ensureAllClaudeDirectories(): void {
|
||||
this.ensureDirectories([
|
||||
this.getClaudeConfigDirectory(),
|
||||
this.getClaudeCommandsDirectory()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract project name from a file path (improved from PathResolver)
|
||||
*/
|
||||
static extractProjectName(filePath: string): string {
|
||||
const pathParts = filePath.split(sep);
|
||||
|
||||
// Look for common project indicators
|
||||
const projectIndicators = ['src', 'lib', 'app', 'project', 'workspace'];
|
||||
for (let i = pathParts.length - 1; i >= 0; i--) {
|
||||
if (projectIndicators.includes(pathParts[i]) && i > 0) {
|
||||
return pathParts[i - 1];
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to directory containing the file
|
||||
if (pathParts.length > 1) {
|
||||
return pathParts[pathParts.length - 2];
|
||||
}
|
||||
|
||||
return 'unknown-project';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current project directory name
|
||||
* Uses git repository root's basename if in a git repo, otherwise falls back to cwd basename
|
||||
*/
|
||||
static getCurrentProjectName(): string {
|
||||
try {
|
||||
const gitRoot = execSync('git rev-parse --show-toplevel', {
|
||||
cwd: process.cwd(),
|
||||
encoding: 'utf8',
|
||||
stdio: ['pipe', 'pipe', 'ignore']
|
||||
}).trim();
|
||||
return require('path').basename(gitRoot);
|
||||
} catch {
|
||||
return require('path').basename(process.cwd());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a timestamped backup filename
|
||||
*/
|
||||
static createBackupFilename(originalPath: string): string {
|
||||
const timestamp = new Date()
|
||||
.toISOString()
|
||||
.replace(/[:.]/g, '-')
|
||||
.replace('T', '_')
|
||||
.slice(0, 19);
|
||||
|
||||
return `${originalPath}.backup.${timestamp}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a path exists and is accessible
|
||||
*/
|
||||
static isPathAccessible(path: string): boolean {
|
||||
return existsSync(path) && statSync(path).isDirectory();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
import Database from 'better-sqlite3';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import { PathDiscovery } from '../path-discovery.js';
|
||||
|
||||
export interface Migration {
|
||||
version: number;
|
||||
up: (db: Database.Database) => void;
|
||||
down?: (db: Database.Database) => void;
|
||||
}
|
||||
|
||||
let dbInstance: Database.Database | null = null;
|
||||
|
||||
/**
|
||||
* SQLite Database singleton with migration support and optimized settings
|
||||
*/
|
||||
export class DatabaseManager {
|
||||
private static instance: DatabaseManager;
|
||||
private db: Database.Database | null = null;
|
||||
private migrations: Migration[] = [];
|
||||
|
||||
static getInstance(): DatabaseManager {
|
||||
if (!DatabaseManager.instance) {
|
||||
DatabaseManager.instance = new DatabaseManager();
|
||||
}
|
||||
return DatabaseManager.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a migration to be run during initialization
|
||||
*/
|
||||
registerMigration(migration: Migration): void {
|
||||
this.migrations.push(migration);
|
||||
// Keep migrations sorted by version
|
||||
this.migrations.sort((a, b) => a.version - b.version);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize database connection with optimized settings
|
||||
*/
|
||||
async initialize(): Promise<Database.Database> {
|
||||
if (this.db) {
|
||||
return this.db;
|
||||
}
|
||||
|
||||
// Ensure the data directory exists
|
||||
const dataDir = PathDiscovery.getInstance().getDataDirectory();
|
||||
fs.mkdirSync(dataDir, { recursive: true });
|
||||
|
||||
const dbPath = path.join(dataDir, 'claude-mem.db');
|
||||
this.db = new Database(dbPath);
|
||||
|
||||
// Apply optimized SQLite settings
|
||||
this.db.pragma('journal_mode = WAL');
|
||||
this.db.pragma('synchronous = NORMAL');
|
||||
this.db.pragma('foreign_keys = ON');
|
||||
this.db.pragma('temp_store = memory');
|
||||
this.db.pragma('mmap_size = 268435456'); // 256MB
|
||||
this.db.pragma('cache_size = 10000');
|
||||
|
||||
// Initialize schema_versions table
|
||||
this.initializeSchemaVersions();
|
||||
|
||||
// Run migrations
|
||||
await this.runMigrations();
|
||||
|
||||
dbInstance = this.db;
|
||||
return this.db;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current database connection
|
||||
*/
|
||||
getConnection(): Database.Database {
|
||||
if (!this.db) {
|
||||
throw new Error('Database not initialized. Call initialize() first.');
|
||||
}
|
||||
return this.db;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a function within a transaction
|
||||
*/
|
||||
withTransaction<T>(fn: (db: Database.Database) => T): T {
|
||||
const db = this.getConnection();
|
||||
const transaction = db.transaction(fn);
|
||||
return transaction(db);
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the database connection
|
||||
*/
|
||||
close(): void {
|
||||
if (this.db) {
|
||||
this.db.close();
|
||||
this.db = null;
|
||||
dbInstance = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the schema_versions table
|
||||
*/
|
||||
private initializeSchemaVersions(): void {
|
||||
if (!this.db) return;
|
||||
|
||||
this.db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS schema_versions (
|
||||
id INTEGER PRIMARY KEY,
|
||||
version INTEGER UNIQUE NOT NULL,
|
||||
applied_at TEXT NOT NULL
|
||||
)
|
||||
`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run all pending migrations
|
||||
*/
|
||||
private async runMigrations(): Promise<void> {
|
||||
if (!this.db) return;
|
||||
|
||||
const appliedVersions = this.db
|
||||
.prepare('SELECT version FROM schema_versions ORDER BY version')
|
||||
.all()
|
||||
.map((row: any) => row.version);
|
||||
|
||||
const maxApplied = appliedVersions.length > 0 ? Math.max(...appliedVersions) : 0;
|
||||
|
||||
for (const migration of this.migrations) {
|
||||
if (migration.version > maxApplied) {
|
||||
console.log(`Applying migration ${migration.version}...`);
|
||||
|
||||
const transaction = this.db.transaction(() => {
|
||||
migration.up(this.db!);
|
||||
|
||||
this.db!
|
||||
.prepare('INSERT INTO schema_versions (version, applied_at) VALUES (?, ?)')
|
||||
.run(migration.version, new Date().toISOString());
|
||||
});
|
||||
|
||||
transaction();
|
||||
console.log(`Migration ${migration.version} applied successfully`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current schema version
|
||||
*/
|
||||
getCurrentVersion(): number {
|
||||
if (!this.db) return 0;
|
||||
|
||||
const result = this.db
|
||||
.prepare('SELECT MAX(version) as version FROM schema_versions')
|
||||
.get() as { version: number } | undefined;
|
||||
|
||||
return result?.version || 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the global database instance (for compatibility)
|
||||
*/
|
||||
export function getDatabase(): Database.Database {
|
||||
if (!dbInstance) {
|
||||
throw new Error('Database not initialized. Call DatabaseManager.getInstance().initialize() first.');
|
||||
}
|
||||
return dbInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize and get database manager
|
||||
*/
|
||||
export async function initializeDatabase(): Promise<Database.Database> {
|
||||
const manager = DatabaseManager.getInstance();
|
||||
return await manager.initialize();
|
||||
}
|
||||
|
||||
export { Database };
|
||||
@@ -0,0 +1,229 @@
|
||||
import { Database } from 'better-sqlite3';
|
||||
import { getDatabase } from './Database.js';
|
||||
import { DiagnosticRow, DiagnosticInput, normalizeTimestamp } from './types.js';
|
||||
|
||||
/**
|
||||
* Data Access Object for diagnostic records
|
||||
*/
|
||||
export class DiagnosticsStore {
|
||||
private db: Database.Database;
|
||||
|
||||
constructor(db?: Database.Database) {
|
||||
this.db = db || getDatabase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new diagnostic record
|
||||
*/
|
||||
create(input: DiagnosticInput): DiagnosticRow {
|
||||
const { isoString, epoch } = normalizeTimestamp(input.created_at);
|
||||
|
||||
const stmt = this.db.prepare(`
|
||||
INSERT INTO diagnostics (
|
||||
session_id, message, severity, created_at, created_at_epoch, project, origin
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
const info = stmt.run(
|
||||
input.session_id || null,
|
||||
input.message,
|
||||
input.severity || 'warn',
|
||||
isoString,
|
||||
epoch,
|
||||
input.project,
|
||||
input.origin || 'compressor'
|
||||
);
|
||||
|
||||
return this.getById(info.lastInsertRowid as number)!;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get diagnostic by primary key
|
||||
*/
|
||||
getById(id: number): DiagnosticRow | null {
|
||||
const stmt = this.db.prepare('SELECT * FROM diagnostics WHERE id = ?');
|
||||
return stmt.get(id) as DiagnosticRow || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get diagnostics for a specific session
|
||||
*/
|
||||
getBySessionId(sessionId: string): DiagnosticRow[] {
|
||||
const stmt = this.db.prepare(`
|
||||
SELECT * FROM diagnostics
|
||||
WHERE session_id = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
`);
|
||||
return stmt.all(sessionId) as DiagnosticRow[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recent diagnostics for a project
|
||||
*/
|
||||
getRecentForProject(project: string, limit = 10): DiagnosticRow[] {
|
||||
const stmt = this.db.prepare(`
|
||||
SELECT * FROM diagnostics
|
||||
WHERE project = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`);
|
||||
return stmt.all(project, limit) as DiagnosticRow[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recent diagnostics across all projects
|
||||
*/
|
||||
getRecent(limit = 10): DiagnosticRow[] {
|
||||
const stmt = this.db.prepare(`
|
||||
SELECT * FROM diagnostics
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`);
|
||||
return stmt.all(limit) as DiagnosticRow[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get diagnostics by severity level
|
||||
*/
|
||||
getBySeverity(severity: 'info' | 'warn' | 'error', limit?: number): DiagnosticRow[] {
|
||||
const query = limit
|
||||
? 'SELECT * FROM diagnostics WHERE severity = ? ORDER BY created_at_epoch DESC LIMIT ?'
|
||||
: 'SELECT * FROM diagnostics WHERE severity = ? ORDER BY created_at_epoch DESC';
|
||||
|
||||
const stmt = this.db.prepare(query);
|
||||
const params = limit ? [severity, limit] : [severity];
|
||||
return stmt.all(...params) as DiagnosticRow[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get diagnostics by origin
|
||||
*/
|
||||
getByOrigin(origin: string, limit?: number): DiagnosticRow[] {
|
||||
const query = limit
|
||||
? 'SELECT * FROM diagnostics WHERE origin = ? ORDER BY created_at_epoch DESC LIMIT ?'
|
||||
: 'SELECT * FROM diagnostics WHERE origin = ? ORDER BY created_at_epoch DESC';
|
||||
|
||||
const stmt = this.db.prepare(query);
|
||||
const params = limit ? [origin, limit] : [origin];
|
||||
return stmt.all(...params) as DiagnosticRow[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Search diagnostics by message content
|
||||
*/
|
||||
searchByMessage(query: string, project?: string, limit = 20): DiagnosticRow[] {
|
||||
let sql = 'SELECT * FROM diagnostics WHERE message LIKE ?';
|
||||
const params: any[] = [`%${query}%`];
|
||||
|
||||
if (project) {
|
||||
sql += ' AND project = ?';
|
||||
params.push(project);
|
||||
}
|
||||
|
||||
sql += ' ORDER BY created_at_epoch DESC LIMIT ?';
|
||||
params.push(limit);
|
||||
|
||||
const stmt = this.db.prepare(sql);
|
||||
return stmt.all(...params) as DiagnosticRow[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Count total diagnostics
|
||||
*/
|
||||
count(): number {
|
||||
const stmt = this.db.prepare('SELECT COUNT(*) as count FROM diagnostics');
|
||||
const result = stmt.get() as { count: number };
|
||||
return result.count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count diagnostics by project
|
||||
*/
|
||||
countByProject(project: string): number {
|
||||
const stmt = this.db.prepare('SELECT COUNT(*) as count FROM diagnostics WHERE project = ?');
|
||||
const result = stmt.get(project) as { count: number };
|
||||
return result.count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count diagnostics by severity
|
||||
*/
|
||||
countBySeverity(severity: 'info' | 'warn' | 'error'): number {
|
||||
const stmt = this.db.prepare('SELECT COUNT(*) as count FROM diagnostics WHERE severity = ?');
|
||||
const result = stmt.get(severity) as { count: number };
|
||||
return result.count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a diagnostic record
|
||||
*/
|
||||
update(id: number, input: Partial<DiagnosticInput>): DiagnosticRow {
|
||||
const existing = this.getById(id);
|
||||
if (!existing) {
|
||||
throw new Error(`Diagnostic with id ${id} not found`);
|
||||
}
|
||||
|
||||
const { isoString, epoch } = normalizeTimestamp(input.created_at || existing.created_at);
|
||||
|
||||
const stmt = this.db.prepare(`
|
||||
UPDATE diagnostics SET
|
||||
message = ?, severity = ?, created_at = ?, created_at_epoch = ?, project = ?, origin = ?
|
||||
WHERE id = ?
|
||||
`);
|
||||
|
||||
stmt.run(
|
||||
input.message || existing.message,
|
||||
input.severity || existing.severity,
|
||||
isoString,
|
||||
epoch,
|
||||
input.project || existing.project,
|
||||
input.origin || existing.origin,
|
||||
id
|
||||
);
|
||||
|
||||
return this.getById(id)!;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a diagnostic by ID
|
||||
*/
|
||||
deleteById(id: number): boolean {
|
||||
const stmt = this.db.prepare('DELETE FROM diagnostics WHERE id = ?');
|
||||
const info = stmt.run(id);
|
||||
return info.changes > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete diagnostics by session_id
|
||||
*/
|
||||
deleteBySessionId(sessionId: string): number {
|
||||
const stmt = this.db.prepare('DELETE FROM diagnostics WHERE session_id = ?');
|
||||
const info = stmt.run(sessionId);
|
||||
return info.changes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get unique projects from diagnostics
|
||||
*/
|
||||
getUniqueProjects(): string[] {
|
||||
const stmt = this.db.prepare('SELECT DISTINCT project FROM diagnostics ORDER BY project');
|
||||
const rows = stmt.all() as { project: string }[];
|
||||
return rows.map(row => row.project);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get diagnostic summary stats
|
||||
*/
|
||||
getStats(): { total: number; info: number; warn: number; error: number } {
|
||||
const stmt = this.db.prepare(`
|
||||
SELECT
|
||||
COUNT(*) as total,
|
||||
COUNT(CASE WHEN severity = 'info' THEN 1 END) as info,
|
||||
COUNT(CASE WHEN severity = 'warn' THEN 1 END) as warn,
|
||||
COUNT(CASE WHEN severity = 'error' THEN 1 END) as error
|
||||
FROM diagnostics
|
||||
`);
|
||||
|
||||
return stmt.get() as { total: number; info: number; warn: number; error: number };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,287 @@
|
||||
import { Database } from 'better-sqlite3';
|
||||
import { getDatabase } from './Database.js';
|
||||
import { MemoryRow, MemoryInput, normalizeTimestamp } from './types.js';
|
||||
|
||||
/**
|
||||
* Data Access Object for memory records
|
||||
*/
|
||||
export class MemoryStore {
|
||||
private db: Database.Database;
|
||||
|
||||
constructor(db?: Database.Database) {
|
||||
this.db = db || getDatabase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new memory record
|
||||
*/
|
||||
create(input: MemoryInput): MemoryRow {
|
||||
const { isoString, epoch } = normalizeTimestamp(input.created_at);
|
||||
|
||||
const stmt = this.db.prepare(`
|
||||
INSERT INTO memories (
|
||||
session_id, text, document_id, keywords, created_at, created_at_epoch,
|
||||
project, archive_basename, origin, title, subtitle, facts, concepts, files_touched
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
const info = stmt.run(
|
||||
input.session_id,
|
||||
input.text,
|
||||
input.document_id || null,
|
||||
input.keywords || null,
|
||||
isoString,
|
||||
epoch,
|
||||
input.project,
|
||||
input.archive_basename || null,
|
||||
input.origin || 'transcript',
|
||||
input.title || null,
|
||||
input.subtitle || null,
|
||||
input.facts || null,
|
||||
input.concepts || null,
|
||||
input.files_touched || null
|
||||
);
|
||||
|
||||
return this.getById(info.lastInsertRowid as number)!;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create multiple memory records in a transaction
|
||||
*/
|
||||
createMany(inputs: MemoryInput[]): MemoryRow[] {
|
||||
const transaction = this.db.transaction((memories: MemoryInput[]) => {
|
||||
const results: MemoryRow[] = [];
|
||||
for (const memory of memories) {
|
||||
results.push(this.create(memory));
|
||||
}
|
||||
return results;
|
||||
});
|
||||
|
||||
return transaction(inputs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get memory by primary key
|
||||
*/
|
||||
getById(id: number): MemoryRow | null {
|
||||
const stmt = this.db.prepare('SELECT * FROM memories WHERE id = ?');
|
||||
return stmt.get(id) as MemoryRow || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get memory by document_id
|
||||
*/
|
||||
getByDocumentId(documentId: string): MemoryRow | null {
|
||||
const stmt = this.db.prepare('SELECT * FROM memories WHERE document_id = ?');
|
||||
return stmt.get(documentId) as MemoryRow || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a document_id already exists
|
||||
*/
|
||||
hasDocumentId(documentId: string): boolean {
|
||||
const stmt = this.db.prepare('SELECT 1 FROM memories WHERE document_id = ? LIMIT 1');
|
||||
return Boolean(stmt.get(documentId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get memories for a specific session
|
||||
*/
|
||||
getBySessionId(sessionId: string): MemoryRow[] {
|
||||
const stmt = this.db.prepare(`
|
||||
SELECT * FROM memories
|
||||
WHERE session_id = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
`);
|
||||
return stmt.all(sessionId) as MemoryRow[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recent memories for a project
|
||||
*/
|
||||
getRecentForProject(project: string, limit = 10): MemoryRow[] {
|
||||
const stmt = this.db.prepare(`
|
||||
SELECT * FROM memories
|
||||
WHERE project = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`);
|
||||
return stmt.all(project, limit) as MemoryRow[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recent memories across all projects
|
||||
*/
|
||||
getRecent(limit = 10): MemoryRow[] {
|
||||
const stmt = this.db.prepare(`
|
||||
SELECT * FROM memories
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`);
|
||||
return stmt.all(limit) as MemoryRow[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Search memories by text content
|
||||
*/
|
||||
searchByText(query: string, project?: string, limit = 20): MemoryRow[] {
|
||||
let sql = 'SELECT * FROM memories WHERE text LIKE ?';
|
||||
const params: any[] = [`%${query}%`];
|
||||
|
||||
if (project) {
|
||||
sql += ' AND project = ?';
|
||||
params.push(project);
|
||||
}
|
||||
|
||||
sql += ' ORDER BY created_at_epoch DESC LIMIT ?';
|
||||
params.push(limit);
|
||||
|
||||
const stmt = this.db.prepare(sql);
|
||||
return stmt.all(...params) as MemoryRow[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Search memories by keywords
|
||||
*/
|
||||
searchByKeywords(keywords: string, project?: string, limit = 20): MemoryRow[] {
|
||||
let sql = 'SELECT * FROM memories WHERE keywords LIKE ?';
|
||||
const params: any[] = [`%${keywords}%`];
|
||||
|
||||
if (project) {
|
||||
sql += ' AND project = ?';
|
||||
params.push(project);
|
||||
}
|
||||
|
||||
sql += ' ORDER BY created_at_epoch DESC LIMIT ?';
|
||||
params.push(limit);
|
||||
|
||||
const stmt = this.db.prepare(sql);
|
||||
return stmt.all(...params) as MemoryRow[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get memories by origin type
|
||||
*/
|
||||
getByOrigin(origin: string, limit?: number): MemoryRow[] {
|
||||
const query = limit
|
||||
? 'SELECT * FROM memories WHERE origin = ? ORDER BY created_at_epoch DESC LIMIT ?'
|
||||
: 'SELECT * FROM memories WHERE origin = ? ORDER BY created_at_epoch DESC';
|
||||
|
||||
const stmt = this.db.prepare(query);
|
||||
const params = limit ? [origin, limit] : [origin];
|
||||
return stmt.all(...params) as MemoryRow[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recent memories for a project filtered by origin
|
||||
*/
|
||||
getRecentForProjectByOrigin(project: string, origin: string, limit = 10): MemoryRow[] {
|
||||
const stmt = this.db.prepare(`
|
||||
SELECT * FROM memories
|
||||
WHERE project = ? AND origin = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`);
|
||||
return stmt.all(project, origin, limit) as MemoryRow[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get last N memories for a project, sorted oldest to newest
|
||||
*/
|
||||
getLastNForProject(project: string, limit = 10): MemoryRow[] {
|
||||
const stmt = this.db.prepare(`
|
||||
SELECT * FROM (
|
||||
SELECT * FROM memories
|
||||
WHERE project = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
)
|
||||
ORDER BY created_at_epoch ASC
|
||||
`);
|
||||
return stmt.all(project, limit) as MemoryRow[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Count total memories
|
||||
*/
|
||||
count(): number {
|
||||
const stmt = this.db.prepare('SELECT COUNT(*) as count FROM memories');
|
||||
const result = stmt.get() as { count: number };
|
||||
return result.count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count memories by project
|
||||
*/
|
||||
countByProject(project: string): number {
|
||||
const stmt = this.db.prepare('SELECT COUNT(*) as count FROM memories WHERE project = ?');
|
||||
const result = stmt.get(project) as { count: number };
|
||||
return result.count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a memory record
|
||||
*/
|
||||
update(id: number, input: Partial<MemoryInput>): MemoryRow {
|
||||
const existing = this.getById(id);
|
||||
if (!existing) {
|
||||
throw new Error(`Memory with id ${id} not found`);
|
||||
}
|
||||
|
||||
const { isoString, epoch } = normalizeTimestamp(input.created_at || existing.created_at);
|
||||
|
||||
const stmt = this.db.prepare(`
|
||||
UPDATE memories SET
|
||||
text = ?, document_id = ?, keywords = ?, created_at = ?, created_at_epoch = ?,
|
||||
project = ?, archive_basename = ?, origin = ?, title = ?, subtitle = ?, facts = ?,
|
||||
concepts = ?, files_touched = ?
|
||||
WHERE id = ?
|
||||
`);
|
||||
|
||||
stmt.run(
|
||||
input.text || existing.text,
|
||||
input.document_id !== undefined ? input.document_id : existing.document_id,
|
||||
input.keywords !== undefined ? input.keywords : existing.keywords,
|
||||
isoString,
|
||||
epoch,
|
||||
input.project || existing.project,
|
||||
input.archive_basename !== undefined ? input.archive_basename : existing.archive_basename,
|
||||
input.origin || existing.origin,
|
||||
input.title !== undefined ? input.title : existing.title,
|
||||
input.subtitle !== undefined ? input.subtitle : existing.subtitle,
|
||||
input.facts !== undefined ? input.facts : existing.facts,
|
||||
input.concepts !== undefined ? input.concepts : existing.concepts,
|
||||
input.files_touched !== undefined ? input.files_touched : existing.files_touched,
|
||||
id
|
||||
);
|
||||
|
||||
return this.getById(id)!;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a memory by ID
|
||||
*/
|
||||
deleteById(id: number): boolean {
|
||||
const stmt = this.db.prepare('DELETE FROM memories WHERE id = ?');
|
||||
const info = stmt.run(id);
|
||||
return info.changes > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete memories by session_id
|
||||
*/
|
||||
deleteBySessionId(sessionId: string): number {
|
||||
const stmt = this.db.prepare('DELETE FROM memories WHERE session_id = ?');
|
||||
const info = stmt.run(sessionId);
|
||||
return info.changes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get unique projects from memories
|
||||
*/
|
||||
getUniqueProjects(): string[] {
|
||||
const stmt = this.db.prepare('SELECT DISTINCT project FROM memories ORDER BY project');
|
||||
const rows = stmt.all() as { project: string }[];
|
||||
return rows.map(row => row.project);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
import { Database } from 'better-sqlite3';
|
||||
import { getDatabase } from './Database.js';
|
||||
import { OverviewRow, OverviewInput, normalizeTimestamp } from './types.js';
|
||||
|
||||
/**
|
||||
* Data Access Object for overview records
|
||||
*/
|
||||
export class OverviewStore {
|
||||
private db: Database.Database;
|
||||
|
||||
constructor(db?: Database.Database) {
|
||||
this.db = db || getDatabase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new overview record
|
||||
*/
|
||||
create(input: OverviewInput): OverviewRow {
|
||||
const { isoString, epoch } = normalizeTimestamp(input.created_at);
|
||||
|
||||
const stmt = this.db.prepare(`
|
||||
INSERT INTO overviews (
|
||||
session_id, content, created_at, created_at_epoch, project, origin
|
||||
) VALUES (?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
const info = stmt.run(
|
||||
input.session_id,
|
||||
input.content,
|
||||
isoString,
|
||||
epoch,
|
||||
input.project,
|
||||
input.origin || 'claude'
|
||||
);
|
||||
|
||||
return this.getById(info.lastInsertRowid as number)!;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create or replace an overview for a session (since one session should have one overview)
|
||||
*/
|
||||
upsert(input: OverviewInput): OverviewRow {
|
||||
const existing = this.getBySessionId(input.session_id);
|
||||
if (existing) {
|
||||
return this.update(existing.id, input);
|
||||
}
|
||||
return this.create(input);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get overview by primary key
|
||||
*/
|
||||
getById(id: number): OverviewRow | null {
|
||||
const stmt = this.db.prepare('SELECT * FROM overviews WHERE id = ?');
|
||||
return stmt.get(id) as OverviewRow || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get overview by session_id
|
||||
*/
|
||||
getBySessionId(sessionId: string): OverviewRow | null {
|
||||
const stmt = this.db.prepare('SELECT * FROM overviews WHERE session_id = ?');
|
||||
return stmt.get(sessionId) as OverviewRow || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recent overviews for a project
|
||||
*/
|
||||
getRecentForProject(project: string, limit = 5): OverviewRow[] {
|
||||
const stmt = this.db.prepare(`
|
||||
SELECT * FROM overviews
|
||||
WHERE project = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`);
|
||||
return stmt.all(project, limit) as OverviewRow[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all overviews for a project (oldest to newest)
|
||||
*/
|
||||
getAllForProject(project: string): OverviewRow[] {
|
||||
const stmt = this.db.prepare(`
|
||||
SELECT * FROM overviews
|
||||
WHERE project = ?
|
||||
ORDER BY created_at_epoch ASC
|
||||
`);
|
||||
return stmt.all(project) as OverviewRow[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recent overviews across all projects
|
||||
*/
|
||||
getRecent(limit = 5): OverviewRow[] {
|
||||
const stmt = this.db.prepare(`
|
||||
SELECT * FROM overviews
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`);
|
||||
return stmt.all(limit) as OverviewRow[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Search overviews by content
|
||||
*/
|
||||
searchByContent(query: string, project?: string, limit = 10): OverviewRow[] {
|
||||
let sql = 'SELECT * FROM overviews WHERE content LIKE ?';
|
||||
const params: any[] = [`%${query}%`];
|
||||
|
||||
if (project) {
|
||||
sql += ' AND project = ?';
|
||||
params.push(project);
|
||||
}
|
||||
|
||||
sql += ' ORDER BY created_at_epoch DESC LIMIT ?';
|
||||
params.push(limit);
|
||||
|
||||
const stmt = this.db.prepare(sql);
|
||||
return stmt.all(...params) as OverviewRow[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get overviews by origin type
|
||||
*/
|
||||
getByOrigin(origin: string, limit?: number): OverviewRow[] {
|
||||
const query = limit
|
||||
? 'SELECT * FROM overviews WHERE origin = ? ORDER BY created_at_epoch DESC LIMIT ?'
|
||||
: 'SELECT * FROM overviews WHERE origin = ? ORDER BY created_at_epoch DESC';
|
||||
|
||||
const stmt = this.db.prepare(query);
|
||||
const params = limit ? [origin, limit] : [origin];
|
||||
return stmt.all(...params) as OverviewRow[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Count total overviews
|
||||
*/
|
||||
count(): number {
|
||||
const stmt = this.db.prepare('SELECT COUNT(*) as count FROM overviews');
|
||||
const result = stmt.get() as { count: number };
|
||||
return result.count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count overviews by project
|
||||
*/
|
||||
countByProject(project: string): number {
|
||||
const stmt = this.db.prepare('SELECT COUNT(*) as count FROM overviews WHERE project = ?');
|
||||
const result = stmt.get(project) as { count: number };
|
||||
return result.count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an overview record
|
||||
*/
|
||||
update(id: number, input: Partial<OverviewInput>): OverviewRow {
|
||||
const existing = this.getById(id);
|
||||
if (!existing) {
|
||||
throw new Error(`Overview with id ${id} not found`);
|
||||
}
|
||||
|
||||
const { isoString, epoch } = normalizeTimestamp(input.created_at || existing.created_at);
|
||||
|
||||
const stmt = this.db.prepare(`
|
||||
UPDATE overviews SET
|
||||
content = ?, created_at = ?, created_at_epoch = ?, project = ?, origin = ?
|
||||
WHERE id = ?
|
||||
`);
|
||||
|
||||
stmt.run(
|
||||
input.content || existing.content,
|
||||
isoString,
|
||||
epoch,
|
||||
input.project || existing.project,
|
||||
input.origin || existing.origin,
|
||||
id
|
||||
);
|
||||
|
||||
return this.getById(id)!;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an overview by ID
|
||||
*/
|
||||
deleteById(id: number): boolean {
|
||||
const stmt = this.db.prepare('DELETE FROM overviews WHERE id = ?');
|
||||
const info = stmt.run(id);
|
||||
return info.changes > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete overview by session_id
|
||||
*/
|
||||
deleteBySessionId(sessionId: string): boolean {
|
||||
const stmt = this.db.prepare('DELETE FROM overviews WHERE session_id = ?');
|
||||
const info = stmt.run(sessionId);
|
||||
return info.changes > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get unique projects from overviews
|
||||
*/
|
||||
getUniqueProjects(): string[] {
|
||||
const stmt = this.db.prepare('SELECT DISTINCT project FROM overviews ORDER BY project');
|
||||
const rows = stmt.all() as { project: string }[];
|
||||
return rows.map(row => row.project);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get most recent overview for a specific project
|
||||
*/
|
||||
getByProject(project: string): OverviewRow | null {
|
||||
const stmt = this.db.prepare(`
|
||||
SELECT * FROM overviews
|
||||
WHERE project = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT 1
|
||||
`);
|
||||
return stmt.get(project) as OverviewRow || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create or update overview for a project (keeps only most recent)
|
||||
*/
|
||||
upsertByProject(input: OverviewInput): OverviewRow {
|
||||
const existing = this.getByProject(input.project);
|
||||
if (existing) {
|
||||
return this.update(existing.id, input);
|
||||
}
|
||||
return this.create(input);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete overview by project name
|
||||
*/
|
||||
deleteByProject(project: string): boolean {
|
||||
const stmt = this.db.prepare('DELETE FROM overviews WHERE project = ?');
|
||||
const info = stmt.run(project);
|
||||
return info.changes > 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
import { Database } from 'better-sqlite3';
|
||||
import { getDatabase } from './Database.js';
|
||||
import { SessionRow, SessionInput, normalizeTimestamp } from './types.js';
|
||||
|
||||
/**
|
||||
* Data Access Object for session records
|
||||
*/
|
||||
export class SessionStore {
|
||||
private db: Database.Database;
|
||||
|
||||
constructor(db?: Database.Database) {
|
||||
this.db = db || getDatabase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new session record
|
||||
*/
|
||||
create(input: SessionInput): SessionRow {
|
||||
const { isoString, epoch } = normalizeTimestamp(input.created_at);
|
||||
|
||||
const stmt = this.db.prepare(`
|
||||
INSERT INTO sessions (
|
||||
session_id, project, created_at, created_at_epoch, source,
|
||||
archive_path, archive_bytes, archive_checksum, archived_at, metadata_json
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
const info = stmt.run(
|
||||
input.session_id,
|
||||
input.project,
|
||||
isoString,
|
||||
epoch,
|
||||
input.source || 'compress',
|
||||
input.archive_path || null,
|
||||
input.archive_bytes || null,
|
||||
input.archive_checksum || null,
|
||||
input.archived_at || null,
|
||||
input.metadata_json || null
|
||||
);
|
||||
|
||||
return this.getById(info.lastInsertRowid as number)!;
|
||||
}
|
||||
|
||||
/**
|
||||
* Upsert a session record (insert or update if session_id exists)
|
||||
*/
|
||||
upsert(input: SessionInput): SessionRow {
|
||||
const existing = this.getBySessionId(input.session_id);
|
||||
if (existing) {
|
||||
return this.update(existing.id, input);
|
||||
}
|
||||
return this.create(input);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing session record
|
||||
*/
|
||||
update(id: number, input: Partial<SessionInput>): SessionRow {
|
||||
const existing = this.getById(id);
|
||||
if (!existing) {
|
||||
throw new Error(`Session with id ${id} not found`);
|
||||
}
|
||||
|
||||
const { isoString, epoch } = normalizeTimestamp(input.created_at || existing.created_at);
|
||||
|
||||
const stmt = this.db.prepare(`
|
||||
UPDATE sessions SET
|
||||
project = ?, created_at = ?, created_at_epoch = ?, source = ?,
|
||||
archive_path = ?, archive_bytes = ?, archive_checksum = ?, archived_at = ?, metadata_json = ?
|
||||
WHERE id = ?
|
||||
`);
|
||||
|
||||
stmt.run(
|
||||
input.project || existing.project,
|
||||
isoString,
|
||||
epoch,
|
||||
input.source || existing.source,
|
||||
input.archive_path !== undefined ? input.archive_path : existing.archive_path,
|
||||
input.archive_bytes !== undefined ? input.archive_bytes : existing.archive_bytes,
|
||||
input.archive_checksum !== undefined ? input.archive_checksum : existing.archive_checksum,
|
||||
input.archived_at !== undefined ? input.archived_at : existing.archived_at,
|
||||
input.metadata_json !== undefined ? input.metadata_json : existing.metadata_json,
|
||||
id
|
||||
);
|
||||
|
||||
return this.getById(id)!;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get session by primary key
|
||||
*/
|
||||
getById(id: number): SessionRow | null {
|
||||
const stmt = this.db.prepare('SELECT * FROM sessions WHERE id = ?');
|
||||
return stmt.get(id) as SessionRow || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get session by session_id
|
||||
*/
|
||||
getBySessionId(sessionId: string): SessionRow | null {
|
||||
const stmt = this.db.prepare('SELECT * FROM sessions WHERE session_id = ?');
|
||||
return stmt.get(sessionId) as SessionRow || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a session exists by session_id
|
||||
*/
|
||||
has(sessionId: string): boolean {
|
||||
const stmt = this.db.prepare('SELECT 1 FROM sessions WHERE session_id = ? LIMIT 1');
|
||||
return Boolean(stmt.get(sessionId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all session_ids as a Set (useful for import-history)
|
||||
*/
|
||||
getAllSessionIds(): Set<string> {
|
||||
const stmt = this.db.prepare('SELECT session_id FROM sessions');
|
||||
const rows = stmt.all() as { session_id: string }[];
|
||||
return new Set(rows.map(row => row.session_id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recent sessions for a project
|
||||
*/
|
||||
getRecentForProject(project: string, limit = 5): SessionRow[] {
|
||||
const stmt = this.db.prepare(`
|
||||
SELECT * FROM sessions
|
||||
WHERE project = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`);
|
||||
return stmt.all(project, limit) as SessionRow[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recent sessions across all projects
|
||||
*/
|
||||
getRecent(limit = 5): SessionRow[] {
|
||||
const stmt = this.db.prepare(`
|
||||
SELECT * FROM sessions
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`);
|
||||
return stmt.all(limit) as SessionRow[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get sessions by source type
|
||||
*/
|
||||
getBySource(source: 'compress' | 'save' | 'legacy-jsonl', limit?: number): SessionRow[] {
|
||||
const query = limit
|
||||
? 'SELECT * FROM sessions WHERE source = ? ORDER BY created_at_epoch DESC LIMIT ?'
|
||||
: 'SELECT * FROM sessions WHERE source = ? ORDER BY created_at_epoch DESC';
|
||||
|
||||
const stmt = this.db.prepare(query);
|
||||
const params = limit ? [source, limit] : [source];
|
||||
return stmt.all(...params) as SessionRow[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Count total sessions
|
||||
*/
|
||||
count(): number {
|
||||
const stmt = this.db.prepare('SELECT COUNT(*) as count FROM sessions');
|
||||
const result = stmt.get() as { count: number };
|
||||
return result.count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count sessions by project
|
||||
*/
|
||||
countByProject(project: string): number {
|
||||
const stmt = this.db.prepare('SELECT COUNT(*) as count FROM sessions WHERE project = ?');
|
||||
const result = stmt.get(project) as { count: number };
|
||||
return result.count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a session by ID (cascades to related records)
|
||||
*/
|
||||
deleteById(id: number): boolean {
|
||||
const stmt = this.db.prepare('DELETE FROM sessions WHERE id = ?');
|
||||
const info = stmt.run(id);
|
||||
return info.changes > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a session by session_id (cascades to related records)
|
||||
*/
|
||||
deleteBySessionId(sessionId: string): boolean {
|
||||
const stmt = this.db.prepare('DELETE FROM sessions WHERE session_id = ?');
|
||||
const info = stmt.run(sessionId);
|
||||
return info.changes > 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
import { Database } from 'better-sqlite3';
|
||||
import { getDatabase } from './Database.js';
|
||||
import { normalizeTimestamp } from './types.js';
|
||||
|
||||
/**
|
||||
* Represents a streaming session row in the database
|
||||
*/
|
||||
export interface StreamingSessionRow {
|
||||
id: number;
|
||||
claude_session_id: string;
|
||||
sdk_session_id?: string;
|
||||
project: string;
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
user_prompt?: string;
|
||||
started_at: string;
|
||||
started_at_epoch: number;
|
||||
updated_at?: string;
|
||||
updated_at_epoch?: number;
|
||||
completed_at?: string;
|
||||
completed_at_epoch?: number;
|
||||
status: 'active' | 'completed' | 'failed';
|
||||
}
|
||||
|
||||
/**
|
||||
* Input type for creating a new streaming session
|
||||
*/
|
||||
export interface StreamingSessionInput {
|
||||
claude_session_id: string;
|
||||
project: string;
|
||||
user_prompt?: string;
|
||||
started_at?: string | Date | number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Input type for updating a streaming session
|
||||
*/
|
||||
export interface StreamingSessionUpdate {
|
||||
sdk_session_id?: string;
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
status?: 'active' | 'completed' | 'failed';
|
||||
completed_at?: string | Date | number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Data Access Object for streaming session records
|
||||
* Handles real-time session tracking during SDK compression
|
||||
*/
|
||||
export class StreamingSessionStore {
|
||||
private db: Database.Database;
|
||||
|
||||
constructor(db?: Database.Database) {
|
||||
this.db = db || getDatabase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new streaming session record
|
||||
* This should be called immediately when the hook receives a user prompt
|
||||
*/
|
||||
create(input: StreamingSessionInput): StreamingSessionRow {
|
||||
const { isoString, epoch } = normalizeTimestamp(input.started_at);
|
||||
|
||||
const stmt = this.db.prepare(`
|
||||
INSERT INTO streaming_sessions (
|
||||
claude_session_id, project, user_prompt, started_at, started_at_epoch, status
|
||||
) VALUES (?, ?, ?, ?, ?, 'active')
|
||||
`);
|
||||
|
||||
const info = stmt.run(
|
||||
input.claude_session_id,
|
||||
input.project,
|
||||
input.user_prompt || null,
|
||||
isoString,
|
||||
epoch
|
||||
);
|
||||
|
||||
return this.getById(info.lastInsertRowid as number)!;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a streaming session by internal ID
|
||||
* Uses atomic transaction to prevent race conditions
|
||||
*/
|
||||
update(id: number, updates: StreamingSessionUpdate): StreamingSessionRow {
|
||||
const { isoString: updatedAt, epoch: updatedEpoch } = normalizeTimestamp(new Date());
|
||||
|
||||
const existing = this.getById(id);
|
||||
if (!existing) {
|
||||
throw new Error(`Streaming session with id ${id} not found`);
|
||||
}
|
||||
|
||||
const parts: string[] = [];
|
||||
const values: any[] = [];
|
||||
|
||||
if (updates.sdk_session_id !== undefined) {
|
||||
parts.push('sdk_session_id = ?');
|
||||
values.push(updates.sdk_session_id);
|
||||
}
|
||||
if (updates.title !== undefined) {
|
||||
parts.push('title = ?');
|
||||
values.push(updates.title);
|
||||
}
|
||||
if (updates.subtitle !== undefined) {
|
||||
parts.push('subtitle = ?');
|
||||
values.push(updates.subtitle);
|
||||
}
|
||||
if (updates.status !== undefined) {
|
||||
parts.push('status = ?');
|
||||
values.push(updates.status);
|
||||
}
|
||||
if (updates.completed_at !== undefined) {
|
||||
const { isoString, epoch } = normalizeTimestamp(updates.completed_at);
|
||||
parts.push('completed_at = ?', 'completed_at_epoch = ?');
|
||||
values.push(isoString, epoch);
|
||||
}
|
||||
|
||||
// Always update the updated_at timestamp
|
||||
parts.push('updated_at = ?', 'updated_at_epoch = ?');
|
||||
values.push(updatedAt, updatedEpoch);
|
||||
|
||||
values.push(id);
|
||||
|
||||
const stmt = this.db.prepare(`
|
||||
UPDATE streaming_sessions
|
||||
SET ${parts.join(', ')}
|
||||
WHERE id = ?
|
||||
`);
|
||||
|
||||
stmt.run(...values);
|
||||
|
||||
return this.getById(id)!;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a streaming session by Claude session ID
|
||||
* Convenience method for hooks that only have the Claude session ID
|
||||
*/
|
||||
updateByClaudeSessionId(claudeSessionId: string, updates: StreamingSessionUpdate): StreamingSessionRow | null {
|
||||
const session = this.getByClaudeSessionId(claudeSessionId);
|
||||
if (!session) {
|
||||
return null;
|
||||
}
|
||||
return this.update(session.id, updates);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get streaming session by internal ID
|
||||
*/
|
||||
getById(id: number): StreamingSessionRow | null {
|
||||
const stmt = this.db.prepare('SELECT * FROM streaming_sessions WHERE id = ?');
|
||||
return stmt.get(id) as StreamingSessionRow || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get streaming session by Claude session ID
|
||||
*/
|
||||
getByClaudeSessionId(claudeSessionId: string): StreamingSessionRow | null {
|
||||
const stmt = this.db.prepare('SELECT * FROM streaming_sessions WHERE claude_session_id = ?');
|
||||
return stmt.get(claudeSessionId) as StreamingSessionRow || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get streaming session by SDK session ID
|
||||
*/
|
||||
getBySdkSessionId(sdkSessionId: string): StreamingSessionRow | null {
|
||||
const stmt = this.db.prepare('SELECT * FROM streaming_sessions WHERE sdk_session_id = ?');
|
||||
return stmt.get(sdkSessionId) as StreamingSessionRow || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a streaming session exists by Claude session ID
|
||||
*/
|
||||
has(claudeSessionId: string): boolean {
|
||||
const stmt = this.db.prepare('SELECT 1 FROM streaming_sessions WHERE claude_session_id = ? LIMIT 1');
|
||||
return Boolean(stmt.get(claudeSessionId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get active streaming sessions for a project
|
||||
*/
|
||||
getActiveForProject(project: string): StreamingSessionRow[] {
|
||||
const stmt = this.db.prepare(`
|
||||
SELECT * FROM streaming_sessions
|
||||
WHERE project = ? AND status = 'active'
|
||||
ORDER BY started_at_epoch DESC
|
||||
`);
|
||||
return stmt.all(project) as StreamingSessionRow[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all active streaming sessions
|
||||
*/
|
||||
getAllActive(): StreamingSessionRow[] {
|
||||
const stmt = this.db.prepare(`
|
||||
SELECT * FROM streaming_sessions
|
||||
WHERE status = 'active'
|
||||
ORDER BY started_at_epoch DESC
|
||||
`);
|
||||
return stmt.all() as StreamingSessionRow[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recent streaming sessions (completed or failed)
|
||||
*/
|
||||
getRecent(limit = 10): StreamingSessionRow[] {
|
||||
const stmt = this.db.prepare(`
|
||||
SELECT * FROM streaming_sessions
|
||||
ORDER BY started_at_epoch DESC
|
||||
LIMIT ?
|
||||
`);
|
||||
return stmt.all(limit) as StreamingSessionRow[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a session as completed
|
||||
*/
|
||||
markCompleted(id: number): StreamingSessionRow {
|
||||
return this.update(id, {
|
||||
status: 'completed',
|
||||
completed_at: new Date()
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a session as failed
|
||||
*/
|
||||
markFailed(id: number): StreamingSessionRow {
|
||||
return this.update(id, {
|
||||
status: 'failed',
|
||||
completed_at: new Date()
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a streaming session by ID
|
||||
*/
|
||||
deleteById(id: number): boolean {
|
||||
const stmt = this.db.prepare('DELETE FROM streaming_sessions WHERE id = ?');
|
||||
const info = stmt.run(id);
|
||||
return info.changes > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a streaming session by Claude session ID
|
||||
*/
|
||||
deleteByClaudeSessionId(claudeSessionId: string): boolean {
|
||||
const stmt = this.db.prepare('DELETE FROM streaming_sessions WHERE claude_session_id = ?');
|
||||
const info = stmt.run(claudeSessionId);
|
||||
return info.changes > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up old completed/failed sessions (older than N days)
|
||||
*/
|
||||
cleanupOldSessions(daysOld = 30): number {
|
||||
const cutoffEpoch = Date.now() - (daysOld * 24 * 60 * 60 * 1000);
|
||||
const stmt = this.db.prepare(`
|
||||
DELETE FROM streaming_sessions
|
||||
WHERE status IN ('completed', 'failed')
|
||||
AND completed_at_epoch < ?
|
||||
`);
|
||||
const info = stmt.run(cutoffEpoch);
|
||||
return info.changes;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import { Database } from 'better-sqlite3';
|
||||
import { getDatabase } from './Database.js';
|
||||
import {
|
||||
TranscriptEventInput,
|
||||
TranscriptEventRow,
|
||||
normalizeTimestamp
|
||||
} from './types.js';
|
||||
|
||||
/**
|
||||
* Data access for transcript_events table
|
||||
*/
|
||||
export class TranscriptEventStore {
|
||||
private db: Database.Database;
|
||||
|
||||
constructor(db?: Database.Database) {
|
||||
this.db = db || getDatabase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert or update a transcript event
|
||||
*/
|
||||
upsert(event: TranscriptEventInput): TranscriptEventRow {
|
||||
const { isoString, epoch } = normalizeTimestamp(event.captured_at);
|
||||
|
||||
const stmt = this.db.prepare(`
|
||||
INSERT INTO transcript_events (
|
||||
session_id,
|
||||
project,
|
||||
event_index,
|
||||
event_type,
|
||||
raw_json,
|
||||
captured_at,
|
||||
captured_at_epoch
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(session_id, event_index) DO UPDATE SET
|
||||
project = excluded.project,
|
||||
event_type = excluded.event_type,
|
||||
raw_json = excluded.raw_json,
|
||||
captured_at = excluded.captured_at,
|
||||
captured_at_epoch = excluded.captured_at_epoch
|
||||
`);
|
||||
|
||||
stmt.run(
|
||||
event.session_id,
|
||||
event.project || null,
|
||||
event.event_index,
|
||||
event.event_type || null,
|
||||
event.raw_json,
|
||||
isoString,
|
||||
epoch
|
||||
);
|
||||
|
||||
return this.getBySessionAndIndex(event.session_id, event.event_index)!;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bulk upsert events in a single transaction
|
||||
*/
|
||||
upsertMany(events: TranscriptEventInput[]): TranscriptEventRow[] {
|
||||
const transaction = this.db.transaction((rows: TranscriptEventInput[]) => {
|
||||
const results: TranscriptEventRow[] = [];
|
||||
for (const row of rows) {
|
||||
results.push(this.upsert(row));
|
||||
}
|
||||
return results;
|
||||
});
|
||||
|
||||
return transaction(events);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get event by session and index
|
||||
*/
|
||||
getBySessionAndIndex(sessionId: string, eventIndex: number): TranscriptEventRow | null {
|
||||
const stmt = this.db.prepare(`
|
||||
SELECT * FROM transcript_events
|
||||
WHERE session_id = ? AND event_index = ?
|
||||
`);
|
||||
return stmt.get(sessionId, eventIndex) as TranscriptEventRow | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get highest event_index stored for a session
|
||||
*/
|
||||
getMaxEventIndex(sessionId: string): number {
|
||||
const stmt = this.db.prepare(`
|
||||
SELECT MAX(event_index) as max_event_index
|
||||
FROM transcript_events
|
||||
WHERE session_id = ?
|
||||
`);
|
||||
const row = stmt.get(sessionId) as { max_event_index: number | null } | undefined;
|
||||
return row?.max_event_index ?? -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* List recent events for a session
|
||||
*/
|
||||
listBySession(sessionId: string, limit = 200, offset = 0): TranscriptEventRow[] {
|
||||
const stmt = this.db.prepare(`
|
||||
SELECT * FROM transcript_events
|
||||
WHERE session_id = ?
|
||||
ORDER BY event_index ASC
|
||||
LIMIT ? OFFSET ?
|
||||
`);
|
||||
return stmt.all(sessionId, limit, offset) as TranscriptEventRow[];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
// Export main components
|
||||
export { DatabaseManager, getDatabase, initializeDatabase } from './Database.js';
|
||||
|
||||
// Export store classes
|
||||
export { SessionStore } from './SessionStore.js';
|
||||
export { MemoryStore } from './MemoryStore.js';
|
||||
export { OverviewStore } from './OverviewStore.js';
|
||||
export { DiagnosticsStore } from './DiagnosticsStore.js';
|
||||
export { TranscriptEventStore } from './TranscriptEventStore.js';
|
||||
export { StreamingSessionStore } from './StreamingSessionStore.js';
|
||||
|
||||
// Export types
|
||||
export * from './types.js';
|
||||
|
||||
// Export migrations
|
||||
export { migrations } from './migrations.js';
|
||||
|
||||
// Convenience function to get all stores
|
||||
export async function createStores() {
|
||||
const { DatabaseManager } = await import('./Database.js');
|
||||
const { migrations } = await import('./migrations.js');
|
||||
|
||||
// Register migrations before initialization
|
||||
const manager = DatabaseManager.getInstance();
|
||||
for (const migration of migrations) {
|
||||
manager.registerMigration(migration);
|
||||
}
|
||||
|
||||
const db = await manager.initialize();
|
||||
|
||||
const { SessionStore } = await import('./SessionStore.js');
|
||||
const { MemoryStore } = await import('./MemoryStore.js');
|
||||
const { OverviewStore } = await import('./OverviewStore.js');
|
||||
const { DiagnosticsStore } = await import('./DiagnosticsStore.js');
|
||||
const { TranscriptEventStore } = await import('./TranscriptEventStore.js');
|
||||
const { StreamingSessionStore } = await import('./StreamingSessionStore.js');
|
||||
|
||||
return {
|
||||
sessions: new SessionStore(db),
|
||||
memories: new MemoryStore(db),
|
||||
overviews: new OverviewStore(db),
|
||||
diagnostics: new DiagnosticsStore(db),
|
||||
transcriptEvents: new TranscriptEventStore(db),
|
||||
streamingSessions: new StreamingSessionStore(db)
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
import { Database } from 'better-sqlite3';
|
||||
import { Migration } from './Database.js';
|
||||
|
||||
/**
|
||||
* Initial schema migration - creates all core tables
|
||||
*/
|
||||
export const migration001: Migration = {
|
||||
version: 1,
|
||||
up: (db: Database.Database) => {
|
||||
// Sessions table - core session tracking
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS sessions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
session_id TEXT UNIQUE NOT NULL,
|
||||
project TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
created_at_epoch INTEGER NOT NULL,
|
||||
source TEXT NOT NULL DEFAULT 'compress',
|
||||
archive_path TEXT,
|
||||
archive_bytes INTEGER,
|
||||
archive_checksum TEXT,
|
||||
archived_at TEXT,
|
||||
metadata_json TEXT
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project);
|
||||
CREATE INDEX IF NOT EXISTS idx_sessions_created_at ON sessions(created_at_epoch DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_sessions_project_created ON sessions(project, created_at_epoch DESC);
|
||||
`);
|
||||
|
||||
// Memories table - compressed memory chunks
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS memories (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
session_id TEXT NOT NULL,
|
||||
text TEXT NOT NULL,
|
||||
document_id TEXT UNIQUE,
|
||||
keywords TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
created_at_epoch INTEGER NOT NULL,
|
||||
project TEXT NOT NULL,
|
||||
archive_basename TEXT,
|
||||
origin TEXT NOT NULL DEFAULT 'transcript',
|
||||
FOREIGN KEY (session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_memories_session ON memories(session_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_memories_project ON memories(project);
|
||||
CREATE INDEX IF NOT EXISTS idx_memories_created_at ON memories(created_at_epoch DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_memories_project_created ON memories(project, created_at_epoch DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_memories_document_id ON memories(document_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_memories_origin ON memories(origin);
|
||||
`);
|
||||
|
||||
// Overviews table - session summaries (one per project)
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS overviews (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
session_id TEXT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
created_at_epoch INTEGER NOT NULL,
|
||||
project TEXT NOT NULL,
|
||||
origin TEXT NOT NULL DEFAULT 'claude',
|
||||
FOREIGN KEY (session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_overviews_session ON overviews(session_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_overviews_project ON overviews(project);
|
||||
CREATE INDEX IF NOT EXISTS idx_overviews_created_at ON overviews(created_at_epoch DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_overviews_project_created ON overviews(project, created_at_epoch DESC);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_overviews_project_latest ON overviews(project, created_at_epoch DESC);
|
||||
`);
|
||||
|
||||
// Diagnostics table - system health and debug info
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS diagnostics (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
session_id TEXT,
|
||||
message TEXT NOT NULL,
|
||||
severity TEXT NOT NULL DEFAULT 'info',
|
||||
created_at TEXT NOT NULL,
|
||||
created_at_epoch INTEGER NOT NULL,
|
||||
project TEXT NOT NULL,
|
||||
origin TEXT NOT NULL DEFAULT 'system',
|
||||
FOREIGN KEY (session_id) REFERENCES sessions(session_id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_diagnostics_session ON diagnostics(session_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_diagnostics_project ON diagnostics(project);
|
||||
CREATE INDEX IF NOT EXISTS idx_diagnostics_severity ON diagnostics(severity);
|
||||
CREATE INDEX IF NOT EXISTS idx_diagnostics_created ON diagnostics(created_at_epoch DESC);
|
||||
`);
|
||||
|
||||
// Transcript events table - raw conversation events
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS transcript_events (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
session_id TEXT NOT NULL,
|
||||
project TEXT,
|
||||
event_index INTEGER NOT NULL,
|
||||
event_type TEXT,
|
||||
raw_json TEXT NOT NULL,
|
||||
captured_at TEXT NOT NULL,
|
||||
captured_at_epoch INTEGER NOT NULL,
|
||||
FOREIGN KEY (session_id) REFERENCES sessions(session_id) ON DELETE CASCADE,
|
||||
UNIQUE(session_id, event_index)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_transcript_events_session ON transcript_events(session_id, event_index);
|
||||
CREATE INDEX IF NOT EXISTS idx_transcript_events_project ON transcript_events(project);
|
||||
CREATE INDEX IF NOT EXISTS idx_transcript_events_type ON transcript_events(event_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_transcript_events_captured ON transcript_events(captured_at_epoch DESC);
|
||||
`);
|
||||
|
||||
console.log('✅ Created all database tables successfully');
|
||||
},
|
||||
|
||||
down: (db: Database.Database) => {
|
||||
db.exec(`
|
||||
DROP TABLE IF EXISTS transcript_events;
|
||||
DROP TABLE IF EXISTS diagnostics;
|
||||
DROP TABLE IF EXISTS overviews;
|
||||
DROP TABLE IF EXISTS memories;
|
||||
DROP TABLE IF EXISTS sessions;
|
||||
`);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Migration 002 - Add hierarchical memory fields (v2 format)
|
||||
*/
|
||||
export const migration002: Migration = {
|
||||
version: 2,
|
||||
up: (db: Database.Database) => {
|
||||
// Add new columns for hierarchical memory structure
|
||||
db.exec(`
|
||||
ALTER TABLE memories ADD COLUMN title TEXT;
|
||||
ALTER TABLE memories ADD COLUMN subtitle TEXT;
|
||||
ALTER TABLE memories ADD COLUMN facts TEXT;
|
||||
ALTER TABLE memories ADD COLUMN concepts TEXT;
|
||||
ALTER TABLE memories ADD COLUMN files_touched TEXT;
|
||||
`);
|
||||
|
||||
// Create indexes for the new fields to improve search performance
|
||||
db.exec(`
|
||||
CREATE INDEX IF NOT EXISTS idx_memories_title ON memories(title);
|
||||
CREATE INDEX IF NOT EXISTS idx_memories_concepts ON memories(concepts);
|
||||
`);
|
||||
|
||||
console.log('✅ Added hierarchical memory fields to memories table');
|
||||
},
|
||||
|
||||
down: (db: Database.Database) => {
|
||||
// Note: SQLite doesn't support DROP COLUMN in all versions
|
||||
// In production, we'd need to recreate the table without these columns
|
||||
// For now, we'll just log a warning
|
||||
console.log('⚠️ Warning: SQLite ALTER TABLE DROP COLUMN not fully supported');
|
||||
console.log('⚠️ To rollback, manually recreate the memories table');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Migration 003 - Add streaming_sessions table for real-time session tracking
|
||||
*/
|
||||
export const migration003: Migration = {
|
||||
version: 3,
|
||||
up: (db: Database.Database) => {
|
||||
// Streaming sessions table - tracks active SDK compression sessions
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS streaming_sessions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
claude_session_id TEXT UNIQUE NOT NULL,
|
||||
sdk_session_id TEXT,
|
||||
project TEXT NOT NULL,
|
||||
title TEXT,
|
||||
subtitle TEXT,
|
||||
user_prompt TEXT,
|
||||
started_at TEXT NOT NULL,
|
||||
started_at_epoch INTEGER NOT NULL,
|
||||
updated_at TEXT,
|
||||
updated_at_epoch INTEGER,
|
||||
completed_at TEXT,
|
||||
completed_at_epoch INTEGER,
|
||||
status TEXT NOT NULL DEFAULT 'active'
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_streaming_sessions_claude_id ON streaming_sessions(claude_session_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_streaming_sessions_sdk_id ON streaming_sessions(sdk_session_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_streaming_sessions_project ON streaming_sessions(project);
|
||||
CREATE INDEX IF NOT EXISTS idx_streaming_sessions_status ON streaming_sessions(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_streaming_sessions_started ON streaming_sessions(started_at_epoch DESC);
|
||||
`);
|
||||
|
||||
console.log('✅ Created streaming_sessions table for real-time session tracking');
|
||||
},
|
||||
|
||||
down: (db: Database.Database) => {
|
||||
db.exec(`
|
||||
DROP TABLE IF EXISTS streaming_sessions;
|
||||
`);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* All migrations in order
|
||||
*/
|
||||
export const migrations: Migration[] = [
|
||||
migration001,
|
||||
migration002,
|
||||
migration003
|
||||
];
|
||||
@@ -0,0 +1,184 @@
|
||||
/**
|
||||
* Database entity types for SQLite storage
|
||||
*/
|
||||
|
||||
export interface SessionRow {
|
||||
id: number;
|
||||
session_id: string;
|
||||
project: string;
|
||||
created_at: string;
|
||||
created_at_epoch: number;
|
||||
source: 'compress' | 'save' | 'legacy-jsonl';
|
||||
archive_path?: string;
|
||||
archive_bytes?: number;
|
||||
archive_checksum?: string;
|
||||
archived_at?: string;
|
||||
metadata_json?: string;
|
||||
}
|
||||
|
||||
export interface OverviewRow {
|
||||
id: number;
|
||||
session_id: string;
|
||||
content: string;
|
||||
created_at: string;
|
||||
created_at_epoch: number;
|
||||
project: string;
|
||||
origin: string;
|
||||
}
|
||||
|
||||
export interface MemoryRow {
|
||||
id: number;
|
||||
session_id: string;
|
||||
text: string;
|
||||
document_id?: string;
|
||||
keywords?: string;
|
||||
created_at: string;
|
||||
created_at_epoch: number;
|
||||
project: string;
|
||||
archive_basename?: string;
|
||||
origin: string;
|
||||
// Hierarchical memory fields (v2)
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
facts?: string; // JSON array of fact strings
|
||||
concepts?: string; // JSON array of concept strings
|
||||
files_touched?: string; // JSON array of file paths
|
||||
}
|
||||
|
||||
export interface DiagnosticRow {
|
||||
id: number;
|
||||
session_id?: string;
|
||||
message: string;
|
||||
severity: 'info' | 'warn' | 'error';
|
||||
created_at: string;
|
||||
created_at_epoch: number;
|
||||
project: string;
|
||||
origin: string;
|
||||
}
|
||||
|
||||
export interface TranscriptEventRow {
|
||||
id: number;
|
||||
session_id: string;
|
||||
project?: string;
|
||||
event_index: number;
|
||||
event_type?: string;
|
||||
raw_json: string;
|
||||
captured_at: string;
|
||||
captured_at_epoch: number;
|
||||
}
|
||||
|
||||
export interface ArchiveRow {
|
||||
id: number;
|
||||
session_id: string;
|
||||
path: string;
|
||||
bytes?: number;
|
||||
checksum?: string;
|
||||
stored_at: string;
|
||||
storage_status: 'active' | 'archived' | 'deleted';
|
||||
}
|
||||
|
||||
export interface TitleRow {
|
||||
id: number;
|
||||
session_id: string;
|
||||
title: string;
|
||||
created_at: string;
|
||||
project: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Input types for creating new records (without id and auto-generated fields)
|
||||
*/
|
||||
export interface SessionInput {
|
||||
session_id: string;
|
||||
project: string;
|
||||
created_at: string;
|
||||
source?: 'compress' | 'save' | 'legacy-jsonl';
|
||||
archive_path?: string;
|
||||
archive_bytes?: number;
|
||||
archive_checksum?: string;
|
||||
archived_at?: string;
|
||||
metadata_json?: string;
|
||||
}
|
||||
|
||||
export interface OverviewInput {
|
||||
session_id: string;
|
||||
content: string;
|
||||
created_at: string;
|
||||
project: string;
|
||||
origin?: string;
|
||||
}
|
||||
|
||||
export interface MemoryInput {
|
||||
session_id: string;
|
||||
text: string;
|
||||
document_id?: string;
|
||||
keywords?: string;
|
||||
created_at: string;
|
||||
project: string;
|
||||
archive_basename?: string;
|
||||
origin?: string;
|
||||
// Hierarchical memory fields (v2)
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
facts?: string; // JSON array of fact strings
|
||||
concepts?: string; // JSON array of concept strings
|
||||
files_touched?: string; // JSON array of file paths
|
||||
}
|
||||
|
||||
export interface DiagnosticInput {
|
||||
session_id?: string;
|
||||
message: string;
|
||||
severity?: 'info' | 'warn' | 'error';
|
||||
created_at: string;
|
||||
project: string;
|
||||
origin?: string;
|
||||
}
|
||||
|
||||
export interface TranscriptEventInput {
|
||||
session_id: string;
|
||||
project?: string;
|
||||
event_index: number;
|
||||
event_type?: string;
|
||||
raw_json: string;
|
||||
captured_at?: string | Date | number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to normalize timestamps from various formats
|
||||
*/
|
||||
export function normalizeTimestamp(timestamp: string | Date | number | undefined): { isoString: string; epoch: number } {
|
||||
let date: Date;
|
||||
|
||||
if (!timestamp) {
|
||||
date = new Date();
|
||||
} else if (timestamp instanceof Date) {
|
||||
date = timestamp;
|
||||
} else if (typeof timestamp === 'number') {
|
||||
date = new Date(timestamp);
|
||||
} else if (typeof timestamp === 'string') {
|
||||
// Handle empty strings
|
||||
if (!timestamp.trim()) {
|
||||
date = new Date();
|
||||
} else {
|
||||
date = new Date(timestamp);
|
||||
// If invalid date, try to parse it differently
|
||||
if (isNaN(date.getTime())) {
|
||||
// Try common formats
|
||||
const cleaned = timestamp.replace(/\s+/g, 'T').replace(/T+/g, 'T');
|
||||
date = new Date(cleaned);
|
||||
|
||||
// Still invalid? Use current time
|
||||
if (isNaN(date.getTime())) {
|
||||
date = new Date();
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
date = new Date();
|
||||
}
|
||||
|
||||
return {
|
||||
isoString: date.toISOString(),
|
||||
epoch: date.getTime()
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { readFileSync, existsSync } from 'fs';
|
||||
import { join, dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
// <Block> 5.1 ====================================
|
||||
// Default values
|
||||
const DEFAULT_PACKAGE_NAME = 'claude-mem';
|
||||
// This MUST be replaced by build process with --define flag
|
||||
// @ts-ignore
|
||||
// For development, use fallback
|
||||
const DEFAULT_PACKAGE_VERSION = typeof __DEFAULT_PACKAGE_VERSION__ !== 'undefined'
|
||||
? __DEFAULT_PACKAGE_VERSION__
|
||||
: '3.5.6-dev';
|
||||
const DEFAULT_PACKAGE_DESCRIPTION = 'Memory compression system for Claude Code - persist context across sessions';
|
||||
|
||||
let packageName = DEFAULT_PACKAGE_NAME;
|
||||
let packageVersion = DEFAULT_PACKAGE_VERSION;
|
||||
let packageDescription = DEFAULT_PACKAGE_DESCRIPTION;
|
||||
// </Block> =======================================
|
||||
|
||||
// Try to read package.json if it exists (for development)
|
||||
// <Block> 5.2 ====================================
|
||||
try {
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
const packageJsonPath = join(__dirname, '..', '..', 'package.json');
|
||||
|
||||
// <Block> 5.2a ====================================
|
||||
if (existsSync(packageJsonPath)) {
|
||||
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
|
||||
// <Block> 5.2b ====================================
|
||||
packageName = packageJson.name || DEFAULT_PACKAGE_NAME;
|
||||
packageVersion = packageJson.version || DEFAULT_PACKAGE_VERSION;
|
||||
packageDescription = packageJson.description || DEFAULT_PACKAGE_DESCRIPTION;
|
||||
// </Block> =======================================
|
||||
}
|
||||
// </Block> =======================================
|
||||
} catch {
|
||||
// Use defaults if package.json can't be read
|
||||
}
|
||||
// </Block> =======================================
|
||||
|
||||
// <Block> 5.3 ====================================
|
||||
// Export package configuration
|
||||
export const PACKAGE_NAME = packageName;
|
||||
export const PACKAGE_VERSION = packageVersion;
|
||||
export const PACKAGE_DESCRIPTION = packageDescription;
|
||||
|
||||
// Export commonly used names
|
||||
export const CLI_NAME = PACKAGE_NAME; // The CLI command name
|
||||
// </Block> =======================================
|
||||
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* Simple logging utility for claude-mem
|
||||
*/
|
||||
|
||||
export interface LogLevel {
|
||||
DEBUG: number;
|
||||
INFO: number;
|
||||
WARN: number;
|
||||
ERROR: number;
|
||||
}
|
||||
|
||||
const LOG_LEVELS: LogLevel = {
|
||||
DEBUG: 0,
|
||||
INFO: 1,
|
||||
WARN: 2,
|
||||
ERROR: 3,
|
||||
};
|
||||
|
||||
class Logger {
|
||||
// <Block> 2.1 ====================================
|
||||
private level: number = LOG_LEVELS.INFO;
|
||||
|
||||
setLevel(level: keyof LogLevel): void {
|
||||
this.level = LOG_LEVELS[level];
|
||||
}
|
||||
// </Block> =======================================
|
||||
|
||||
// <Block> 2.2 ====================================
|
||||
debug(message: string, ...args: any[]): void {
|
||||
if (this.level <= LOG_LEVELS.DEBUG) {
|
||||
console.debug(`[DEBUG] ${message}`, ...args);
|
||||
}
|
||||
}
|
||||
// </Block> =======================================
|
||||
|
||||
// <Block> 2.3 ====================================
|
||||
info(message: string, ...args: any[]): void {
|
||||
if (this.level <= LOG_LEVELS.INFO) {
|
||||
console.info(`[INFO] ${message}`, ...args);
|
||||
}
|
||||
}
|
||||
// </Block> =======================================
|
||||
|
||||
// <Block> 2.4 ====================================
|
||||
warn(message: string, ...args: any[]): void {
|
||||
if (this.level <= LOG_LEVELS.WARN) {
|
||||
console.warn(`[WARN] ${message}`, ...args);
|
||||
}
|
||||
}
|
||||
// </Block> =======================================
|
||||
|
||||
// <Block> 2.5 ====================================
|
||||
error(message: string, error?: any, context?: any): void {
|
||||
if (this.level <= LOG_LEVELS.ERROR) {
|
||||
console.error(`[ERROR] ${message}`);
|
||||
if (error) {
|
||||
console.error(error);
|
||||
}
|
||||
if (context) {
|
||||
console.error('Context:', context);
|
||||
}
|
||||
}
|
||||
}
|
||||
// </Block> =======================================
|
||||
}
|
||||
|
||||
export const log = new Logger();
|
||||
@@ -0,0 +1,91 @@
|
||||
import { sep, basename } from 'path';
|
||||
import { PathDiscovery } from '../services/path-discovery.js';
|
||||
|
||||
/**
|
||||
* PathResolver utility for managing claude-mem file system paths
|
||||
* Now delegates to PathDiscovery service for centralized path management
|
||||
*/
|
||||
export class PathResolver {
|
||||
private pathDiscovery: PathDiscovery;
|
||||
|
||||
// <Block> 1.1 ====================================
|
||||
constructor() {
|
||||
this.pathDiscovery = PathDiscovery.getInstance();
|
||||
}
|
||||
// </Block> =======================================
|
||||
|
||||
// <Block> 1.2 ====================================
|
||||
getConfigDir(): string {
|
||||
return this.pathDiscovery.getDataDirectory();
|
||||
}
|
||||
// </Block> =======================================
|
||||
|
||||
// <Block> 1.3 ====================================
|
||||
getIndexDir(): string {
|
||||
return this.pathDiscovery.getIndexDirectory();
|
||||
}
|
||||
// </Block> =======================================
|
||||
|
||||
// <Block> 1.4 ====================================
|
||||
getIndexPath(): string {
|
||||
return this.pathDiscovery.getIndexPath();
|
||||
}
|
||||
// </Block> =======================================
|
||||
|
||||
// <Block> 1.5 ====================================
|
||||
getArchiveDir(): string {
|
||||
return this.pathDiscovery.getArchivesDirectory();
|
||||
}
|
||||
// </Block> =======================================
|
||||
|
||||
// <Block> 1.6 ====================================
|
||||
getProjectArchiveDir(projectName: string): string {
|
||||
return this.pathDiscovery.getProjectArchiveDirectory(projectName);
|
||||
}
|
||||
// </Block> =======================================
|
||||
|
||||
// <Block> 1.7 ====================================
|
||||
getLogsDir(): string {
|
||||
return this.pathDiscovery.getLogsDirectory();
|
||||
}
|
||||
// </Block> =======================================
|
||||
|
||||
// <Block> 1.8 ====================================
|
||||
static ensureDirectory(dirPath: string): void {
|
||||
PathDiscovery.getInstance().ensureDirectory(dirPath);
|
||||
}
|
||||
// </Block> =======================================
|
||||
|
||||
// <Block> 1.9 ====================================
|
||||
static ensureDirectories(dirPaths: string[]): void {
|
||||
PathDiscovery.getInstance().ensureDirectories(dirPaths);
|
||||
}
|
||||
// </Block> =======================================
|
||||
|
||||
// <Block> 1.10 ===================================
|
||||
static extractProjectName(transcriptPath: string): string {
|
||||
return PathDiscovery.extractProjectName(transcriptPath);
|
||||
}
|
||||
|
||||
// <Block> 1.11 ===================================
|
||||
/**
|
||||
* DRY utility function: Canonical source for getting the current project prefix
|
||||
* Replaces all instances of path.basename(process.cwd()) across the codebase
|
||||
* @returns The current project directory name, sanitized for use as a prefix
|
||||
*/
|
||||
static getCurrentProjectPrefix(): string {
|
||||
return PathDiscovery.getCurrentProjectName();
|
||||
}
|
||||
// </Block> =======================================
|
||||
|
||||
// <Block> 1.12 ===================================
|
||||
/**
|
||||
* DRY utility function: Gets raw project name without sanitization
|
||||
* For use in contexts where original directory name is needed (e.g., display)
|
||||
* @returns The current project directory name as-is
|
||||
*/
|
||||
static getCurrentProjectName(): string {
|
||||
return PathDiscovery.getCurrentProjectName();
|
||||
}
|
||||
// </Block> =======================================
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { appendFileSync, existsSync, mkdirSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { PathDiscovery } from '../services/path-discovery.js';
|
||||
|
||||
let logPath: string | null = null;
|
||||
|
||||
function ensureLogPath(): string {
|
||||
if (logPath) {
|
||||
return logPath;
|
||||
}
|
||||
|
||||
const discovery = PathDiscovery.getInstance();
|
||||
const logsDir = discovery.getLogsDirectory();
|
||||
|
||||
if (!existsSync(logsDir)) {
|
||||
mkdirSync(logsDir, { recursive: true });
|
||||
}
|
||||
|
||||
logPath = join(logsDir, 'rolling-memory.log');
|
||||
return logPath;
|
||||
}
|
||||
|
||||
export type RollingLogLevel = 'debug' | 'info' | 'warn' | 'error';
|
||||
|
||||
export function rollingLog(
|
||||
level: RollingLogLevel,
|
||||
message: string,
|
||||
payload: Record<string, unknown> = {}
|
||||
): void {
|
||||
try {
|
||||
const file = ensureLogPath();
|
||||
const entry = {
|
||||
timestamp: new Date().toISOString(),
|
||||
level,
|
||||
message,
|
||||
...payload
|
||||
};
|
||||
appendFileSync(file, `${JSON.stringify(entry)}\n`, 'utf8');
|
||||
} catch {
|
||||
// Logging should never throw user-facing errors
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import { readSettings } from './settings.js';
|
||||
|
||||
export interface RollingSettings {
|
||||
captureEnabled: boolean;
|
||||
summaryEnabled: boolean;
|
||||
sessionStartEnabled: boolean;
|
||||
chunkTokenLimit: number;
|
||||
chunkOverlapTokens: number;
|
||||
summaryTurnLimit: number;
|
||||
}
|
||||
|
||||
const DEFAULTS: RollingSettings = {
|
||||
captureEnabled: true,
|
||||
summaryEnabled: true,
|
||||
sessionStartEnabled: true,
|
||||
chunkTokenLimit: 600,
|
||||
chunkOverlapTokens: 200,
|
||||
summaryTurnLimit: 20
|
||||
};
|
||||
|
||||
function normalizeBoolean(value: unknown, fallback: boolean): boolean {
|
||||
if (typeof value === 'boolean') {
|
||||
return value;
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
const lowered = value.toLowerCase();
|
||||
if (lowered === 'true') return true;
|
||||
if (lowered === 'false') return false;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function normalizeNumber(value: unknown, fallback: number): number {
|
||||
if (typeof value === 'number' && Number.isFinite(value)) {
|
||||
return value;
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
const parsed = Number(value);
|
||||
if (!Number.isNaN(parsed) && Number.isFinite(parsed)) {
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
export function getRollingSettings(): RollingSettings {
|
||||
const settings = readSettings();
|
||||
|
||||
return {
|
||||
captureEnabled: normalizeBoolean(
|
||||
settings.rollingCaptureEnabled,
|
||||
DEFAULTS.captureEnabled
|
||||
),
|
||||
summaryEnabled: normalizeBoolean(
|
||||
settings.rollingSummaryEnabled,
|
||||
DEFAULTS.summaryEnabled
|
||||
),
|
||||
sessionStartEnabled: normalizeBoolean(
|
||||
settings.rollingSessionStartEnabled,
|
||||
DEFAULTS.sessionStartEnabled
|
||||
),
|
||||
chunkTokenLimit: normalizeNumber(
|
||||
settings.rollingChunkTokens,
|
||||
DEFAULTS.chunkTokenLimit
|
||||
),
|
||||
chunkOverlapTokens: normalizeNumber(
|
||||
settings.rollingChunkOverlapTokens,
|
||||
DEFAULTS.chunkOverlapTokens
|
||||
),
|
||||
summaryTurnLimit: normalizeNumber(
|
||||
settings.rollingSummaryTurnLimit,
|
||||
DEFAULTS.summaryTurnLimit
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
export function isRollingCaptureEnabled(): boolean {
|
||||
return getRollingSettings().captureEnabled;
|
||||
}
|
||||
|
||||
export function isRollingSummaryEnabled(): boolean {
|
||||
return getRollingSettings().summaryEnabled;
|
||||
}
|
||||
|
||||
export function isRollingSessionStartEnabled(): boolean {
|
||||
return getRollingSettings().sessionStartEnabled;
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import { readFileSync, existsSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { PathResolver } from './paths.js';
|
||||
import type { Settings } from './types.js';
|
||||
|
||||
/**
|
||||
* Settings utilities for managing ~/.claude-mem/settings.json
|
||||
*/
|
||||
export class SettingsManager {
|
||||
private static settingsPath: string;
|
||||
private static cachedSettings: Settings | null = null;
|
||||
|
||||
static {
|
||||
const pathResolver = new PathResolver();
|
||||
this.settingsPath = join(pathResolver.getConfigDir(), 'settings.json');
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely read settings.json with error handling
|
||||
* Returns empty object if file doesn't exist or is malformed
|
||||
*/
|
||||
static readSettings(): Settings {
|
||||
// Return cached settings if available
|
||||
if (this.cachedSettings !== null) {
|
||||
return this.cachedSettings;
|
||||
}
|
||||
|
||||
try {
|
||||
if (existsSync(this.settingsPath)) {
|
||||
const content = readFileSync(this.settingsPath, 'utf-8');
|
||||
const settings = JSON.parse(content) as Settings;
|
||||
this.cachedSettings = settings;
|
||||
return settings;
|
||||
}
|
||||
} catch {
|
||||
// File is malformed or unreadable - return empty settings
|
||||
}
|
||||
|
||||
// File doesn't exist or failed to read
|
||||
const emptySettings: Settings = {};
|
||||
this.cachedSettings = emptySettings;
|
||||
return emptySettings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific setting value with optional fallback
|
||||
*/
|
||||
static getSetting<K extends keyof Settings>(
|
||||
key: K,
|
||||
fallback?: Settings[K]
|
||||
): Settings[K] | undefined {
|
||||
const settings = this.readSettings();
|
||||
return settings[key] ?? fallback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Claude binary path from settings
|
||||
* Falls back to 'claude' if not found or settings don't exist
|
||||
*/
|
||||
static getClaudePath(): string {
|
||||
const claudePath = this.getSetting('claudePath', 'claude');
|
||||
return claudePath as string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear cached settings (useful for testing or after settings changes)
|
||||
*/
|
||||
static clearCache(): void {
|
||||
this.cachedSettings = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience function to get Claude binary path
|
||||
* Can be imported directly for simple use cases
|
||||
*/
|
||||
export function getClaudePath(): string {
|
||||
return SettingsManager.getClaudePath();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience function to read all settings
|
||||
* Can be imported directly for simple use cases
|
||||
*/
|
||||
export function readSettings(): Settings {
|
||||
return SettingsManager.readSettings();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience function to get a specific setting
|
||||
* Can be imported directly for simple use cases
|
||||
*/
|
||||
export function getSetting<K extends keyof Settings>(
|
||||
key: K,
|
||||
fallback?: Settings[K]
|
||||
): Settings[K] | undefined {
|
||||
return SettingsManager.getSetting(key, fallback);
|
||||
}
|
||||
@@ -0,0 +1,402 @@
|
||||
import fs from 'fs';
|
||||
import { PathDiscovery } from '../services/path-discovery.js';
|
||||
import {
|
||||
createStores,
|
||||
SessionStore,
|
||||
MemoryStore,
|
||||
OverviewStore,
|
||||
DiagnosticsStore,
|
||||
SessionInput,
|
||||
MemoryInput,
|
||||
OverviewInput,
|
||||
DiagnosticInput,
|
||||
SessionRow,
|
||||
MemoryRow,
|
||||
OverviewRow,
|
||||
DiagnosticRow,
|
||||
normalizeTimestamp
|
||||
} from '../services/sqlite/index.js';
|
||||
|
||||
/**
|
||||
* Storage backend types
|
||||
*/
|
||||
export type StorageBackend = 'sqlite' | 'jsonl';
|
||||
|
||||
/**
|
||||
* Unified interface for storage operations
|
||||
*/
|
||||
export interface IStorageProvider {
|
||||
backend: StorageBackend;
|
||||
|
||||
// Session operations
|
||||
createSession(session: SessionInput): Promise<SessionRow | void>;
|
||||
getSession(sessionId: string): Promise<SessionRow | null>;
|
||||
hasSession(sessionId: string): Promise<boolean>;
|
||||
getAllSessionIds(): Promise<Set<string>>;
|
||||
getRecentSessions(limit?: number): Promise<SessionRow[]>;
|
||||
getRecentSessionsForProject(project: string, limit?: number): Promise<SessionRow[]>;
|
||||
|
||||
// Memory operations
|
||||
createMemory(memory: MemoryInput): Promise<MemoryRow | void>;
|
||||
createMemories(memories: MemoryInput[]): Promise<void>;
|
||||
getRecentMemories(limit?: number): Promise<MemoryRow[]>;
|
||||
getRecentMemoriesForProject(project: string, limit?: number): Promise<MemoryRow[]>;
|
||||
hasDocumentId(documentId: string): Promise<boolean>;
|
||||
|
||||
// Overview operations
|
||||
createOverview(overview: OverviewInput): Promise<OverviewRow | void>;
|
||||
upsertOverview(overview: OverviewInput): Promise<OverviewRow | void>;
|
||||
getRecentOverviews(limit?: number): Promise<OverviewRow[]>;
|
||||
getRecentOverviewsForProject(project: string, limit?: number): Promise<OverviewRow[]>;
|
||||
|
||||
// Diagnostic operations
|
||||
createDiagnostic(diagnostic: DiagnosticInput): Promise<DiagnosticRow | void>;
|
||||
|
||||
// Health check
|
||||
isAvailable(): Promise<boolean>;
|
||||
}
|
||||
|
||||
/**
|
||||
* SQLite-based storage provider
|
||||
*/
|
||||
export class SQLiteStorageProvider implements IStorageProvider {
|
||||
public readonly backend = 'sqlite';
|
||||
|
||||
private stores?: {
|
||||
sessions: SessionStore;
|
||||
memories: MemoryStore;
|
||||
overviews: OverviewStore;
|
||||
diagnostics: DiagnosticsStore;
|
||||
};
|
||||
|
||||
private async getStores() {
|
||||
if (!this.stores) {
|
||||
this.stores = await createStores();
|
||||
}
|
||||
return this.stores;
|
||||
}
|
||||
|
||||
async isAvailable(): Promise<boolean> {
|
||||
try {
|
||||
await this.getStores();
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async createSession(session: SessionInput): Promise<SessionRow> {
|
||||
const stores = await this.getStores();
|
||||
return stores.sessions.create(session);
|
||||
}
|
||||
|
||||
async getSession(sessionId: string): Promise<SessionRow | null> {
|
||||
const stores = await this.getStores();
|
||||
return stores.sessions.getBySessionId(sessionId);
|
||||
}
|
||||
|
||||
async hasSession(sessionId: string): Promise<boolean> {
|
||||
const stores = await this.getStores();
|
||||
return stores.sessions.has(sessionId);
|
||||
}
|
||||
|
||||
async getAllSessionIds(): Promise<Set<string>> {
|
||||
const stores = await this.getStores();
|
||||
return stores.sessions.getAllSessionIds();
|
||||
}
|
||||
|
||||
async getRecentSessions(limit = 5): Promise<SessionRow[]> {
|
||||
const stores = await this.getStores();
|
||||
return stores.sessions.getRecent(limit);
|
||||
}
|
||||
|
||||
async getRecentSessionsForProject(project: string, limit = 5): Promise<SessionRow[]> {
|
||||
const stores = await this.getStores();
|
||||
return stores.sessions.getRecentForProject(project, limit);
|
||||
}
|
||||
|
||||
async createMemory(memory: MemoryInput): Promise<MemoryRow> {
|
||||
const stores = await this.getStores();
|
||||
return stores.memories.create(memory);
|
||||
}
|
||||
|
||||
async createMemories(memories: MemoryInput[]): Promise<void> {
|
||||
const stores = await this.getStores();
|
||||
stores.memories.createMany(memories);
|
||||
}
|
||||
|
||||
async getRecentMemories(limit = 10): Promise<MemoryRow[]> {
|
||||
const stores = await this.getStores();
|
||||
return stores.memories.getRecent(limit);
|
||||
}
|
||||
|
||||
async getRecentMemoriesForProject(project: string, limit = 10): Promise<MemoryRow[]> {
|
||||
const stores = await this.getStores();
|
||||
return stores.memories.getRecentForProject(project, limit);
|
||||
}
|
||||
|
||||
async hasDocumentId(documentId: string): Promise<boolean> {
|
||||
const stores = await this.getStores();
|
||||
return stores.memories.hasDocumentId(documentId);
|
||||
}
|
||||
|
||||
async createOverview(overview: OverviewInput): Promise<OverviewRow> {
|
||||
const stores = await this.getStores();
|
||||
return stores.overviews.create(overview);
|
||||
}
|
||||
|
||||
async upsertOverview(overview: OverviewInput): Promise<OverviewRow> {
|
||||
const stores = await this.getStores();
|
||||
return stores.overviews.upsert(overview);
|
||||
}
|
||||
|
||||
async getRecentOverviews(limit = 5): Promise<OverviewRow[]> {
|
||||
const stores = await this.getStores();
|
||||
return stores.overviews.getRecent(limit);
|
||||
}
|
||||
|
||||
async getRecentOverviewsForProject(project: string, limit = 5): Promise<OverviewRow[]> {
|
||||
const stores = await this.getStores();
|
||||
return stores.overviews.getRecentForProject(project, limit);
|
||||
}
|
||||
|
||||
async createDiagnostic(diagnostic: DiagnosticInput): Promise<DiagnosticRow> {
|
||||
const stores = await this.getStores();
|
||||
return stores.diagnostics.create(diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* JSONL-based storage provider (legacy fallback)
|
||||
*/
|
||||
export class JSONLStorageProvider implements IStorageProvider {
|
||||
public readonly backend = 'jsonl';
|
||||
|
||||
private pathDiscovery = PathDiscovery.getInstance();
|
||||
|
||||
async isAvailable(): Promise<boolean> {
|
||||
try {
|
||||
// Ensure data directory exists
|
||||
const dataDir = this.pathDiscovery.getDataDirectory();
|
||||
fs.mkdirSync(dataDir, { recursive: true });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private appendToIndex(obj: any): void {
|
||||
const indexPath = this.pathDiscovery.getIndexPath();
|
||||
fs.appendFileSync(indexPath, JSON.stringify(obj) + '\\n', 'utf8');
|
||||
}
|
||||
|
||||
async createSession(session: SessionInput): Promise<void> {
|
||||
const sessionRecord = {
|
||||
type: 'session',
|
||||
session_id: session.session_id,
|
||||
project: session.project,
|
||||
timestamp: session.created_at
|
||||
};
|
||||
this.appendToIndex(sessionRecord);
|
||||
}
|
||||
|
||||
async getSession(): Promise<null> {
|
||||
// Not supported in JSONL mode
|
||||
return null;
|
||||
}
|
||||
|
||||
async hasSession(sessionId: string): Promise<boolean> {
|
||||
const sessionIds = await this.getAllSessionIds();
|
||||
return sessionIds.has(sessionId);
|
||||
}
|
||||
|
||||
async getAllSessionIds(): Promise<Set<string>> {
|
||||
const indexPath = this.pathDiscovery.getIndexPath();
|
||||
if (!fs.existsSync(indexPath)) {
|
||||
return new Set();
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(indexPath, 'utf-8');
|
||||
const lines = content.trim().split('\\n').filter(line => line.trim());
|
||||
const sessionIds = new Set<string>();
|
||||
|
||||
for (const line of lines) {
|
||||
try {
|
||||
const obj = JSON.parse(line);
|
||||
if (obj.session_id) {
|
||||
sessionIds.add(obj.session_id);
|
||||
}
|
||||
} catch {
|
||||
// Skip malformed JSON
|
||||
}
|
||||
}
|
||||
|
||||
return sessionIds;
|
||||
}
|
||||
|
||||
async getRecentSessions(): Promise<SessionRow[]> {
|
||||
// Not fully supported in JSONL mode - return empty array
|
||||
return [];
|
||||
}
|
||||
|
||||
async getRecentSessionsForProject(): Promise<SessionRow[]> {
|
||||
// Not fully supported in JSONL mode - return empty array
|
||||
return [];
|
||||
}
|
||||
|
||||
async createMemory(memory: MemoryInput): Promise<void> {
|
||||
const memoryRecord = {
|
||||
type: 'memory',
|
||||
text: memory.text,
|
||||
document_id: memory.document_id,
|
||||
keywords: memory.keywords,
|
||||
session_id: memory.session_id,
|
||||
project: memory.project,
|
||||
timestamp: memory.created_at,
|
||||
archive: memory.archive_basename
|
||||
};
|
||||
this.appendToIndex(memoryRecord);
|
||||
}
|
||||
|
||||
async createMemories(memories: MemoryInput[]): Promise<void> {
|
||||
for (const memory of memories) {
|
||||
await this.createMemory(memory);
|
||||
}
|
||||
}
|
||||
|
||||
async getRecentMemories(): Promise<MemoryRow[]> {
|
||||
// Not fully supported in JSONL mode - return empty array
|
||||
return [];
|
||||
}
|
||||
|
||||
async getRecentMemoriesForProject(): Promise<MemoryRow[]> {
|
||||
// Not fully supported in JSONL mode - return empty array
|
||||
return [];
|
||||
}
|
||||
|
||||
async hasDocumentId(documentId: string): Promise<boolean> {
|
||||
const indexPath = this.pathDiscovery.getIndexPath();
|
||||
if (!fs.existsSync(indexPath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(indexPath, 'utf-8');
|
||||
const lines = content.trim().split('\\n').filter(line => line.trim());
|
||||
|
||||
for (const line of lines) {
|
||||
try {
|
||||
const obj = JSON.parse(line);
|
||||
if (obj.type === 'memory' && obj.document_id === documentId) {
|
||||
return true;
|
||||
}
|
||||
} catch {
|
||||
// Skip malformed JSON
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
async createOverview(overview: OverviewInput): Promise<void> {
|
||||
const overviewRecord = {
|
||||
type: 'overview',
|
||||
content: overview.content,
|
||||
session_id: overview.session_id,
|
||||
project: overview.project,
|
||||
timestamp: overview.created_at
|
||||
};
|
||||
this.appendToIndex(overviewRecord);
|
||||
}
|
||||
|
||||
async upsertOverview(overview: OverviewInput): Promise<void> {
|
||||
// Just append in JSONL mode (no real upsert)
|
||||
await this.createOverview(overview);
|
||||
}
|
||||
|
||||
async getRecentOverviews(): Promise<OverviewRow[]> {
|
||||
// Not fully supported in JSONL mode - return empty array
|
||||
return [];
|
||||
}
|
||||
|
||||
async getRecentOverviewsForProject(): Promise<OverviewRow[]> {
|
||||
// Not fully supported in JSONL mode - return empty array
|
||||
return [];
|
||||
}
|
||||
|
||||
async createDiagnostic(diagnostic: DiagnosticInput): Promise<void> {
|
||||
const diagnosticRecord = {
|
||||
type: 'diagnostic',
|
||||
message: diagnostic.message,
|
||||
session_id: diagnostic.session_id,
|
||||
project: diagnostic.project,
|
||||
timestamp: diagnostic.created_at
|
||||
};
|
||||
this.appendToIndex(diagnosticRecord);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Storage provider factory and singleton
|
||||
*/
|
||||
let storageProvider: IStorageProvider | null = null;
|
||||
|
||||
/**
|
||||
* Get the configured storage provider
|
||||
*/
|
||||
export async function getStorageProvider(): Promise<IStorageProvider> {
|
||||
if (storageProvider) {
|
||||
return storageProvider;
|
||||
}
|
||||
|
||||
// Try SQLite first
|
||||
const sqliteProvider = new SQLiteStorageProvider();
|
||||
if (await sqliteProvider.isAvailable()) {
|
||||
storageProvider = sqliteProvider;
|
||||
return storageProvider;
|
||||
}
|
||||
|
||||
// Fall back to JSONL
|
||||
const jsonlProvider = new JSONLStorageProvider();
|
||||
if (await jsonlProvider.isAvailable()) {
|
||||
storageProvider = jsonlProvider;
|
||||
return storageProvider;
|
||||
}
|
||||
|
||||
throw new Error('No storage backend available');
|
||||
}
|
||||
|
||||
/**
|
||||
* Force a specific storage provider (useful for testing)
|
||||
*/
|
||||
export function setStorageProvider(provider: IStorageProvider): void {
|
||||
storageProvider = provider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if SQLite migration is needed
|
||||
*/
|
||||
export async function needsMigration(): Promise<boolean> {
|
||||
const pathDiscovery = PathDiscovery.getInstance();
|
||||
const indexPath = pathDiscovery.getIndexPath();
|
||||
|
||||
// If JSONL exists but SQLite is not available, migration is needed
|
||||
if (fs.existsSync(indexPath)) {
|
||||
const sqliteProvider = new SQLiteStorageProvider();
|
||||
const sqliteAvailable = await sqliteProvider.isAvailable();
|
||||
|
||||
if (!sqliteAvailable) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if SQLite has data
|
||||
try {
|
||||
const stores = await createStores();
|
||||
const sessionCount = stores.sessions.count();
|
||||
return sessionCount === 0; // Needs migration if SQLite is empty
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Core Type Definitions
|
||||
*
|
||||
* Minimal type definitions for the claude-mem system.
|
||||
* Only includes types that are actively imported and used.
|
||||
*/
|
||||
|
||||
// =============================================================================
|
||||
// ERROR CLASSES
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Custom error class for compression failures
|
||||
*/
|
||||
export class CompressionError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
public transcriptPath: string,
|
||||
public stage: 'reading' | 'analyzing' | 'compressing' | 'writing'
|
||||
) {
|
||||
super(message);
|
||||
this.name = 'CompressionError';
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// CONFIGURATION TYPES
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Main settings interface for claude-mem configuration
|
||||
*/
|
||||
export interface Settings {
|
||||
autoCompress?: boolean;
|
||||
projectName?: string;
|
||||
installed?: boolean;
|
||||
backend?: string;
|
||||
embedded?: boolean;
|
||||
saveMemoriesOnClear?: boolean;
|
||||
claudePath?: string;
|
||||
rollingCaptureEnabled?: boolean;
|
||||
rollingSummaryEnabled?: boolean;
|
||||
rollingSessionStartEnabled?: boolean;
|
||||
rollingChunkTokens?: number;
|
||||
rollingChunkOverlapTokens?: number;
|
||||
rollingSummaryTurnLimit?: number;
|
||||
[key: string]: unknown; // Allow additional properties
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import { platform, homedir } from 'os';
|
||||
import { execSync } from 'child_process';
|
||||
import { chmodSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
|
||||
const isWindows = platform() === 'win32';
|
||||
|
||||
/**
|
||||
* Platform-specific utilities for cross-platform compatibility
|
||||
* Handles differences between Windows and Unix-like systems
|
||||
*/
|
||||
export const Platform = {
|
||||
/**
|
||||
* Returns the appropriate shell for the current platform
|
||||
*/
|
||||
getShell: (): string => {
|
||||
return isWindows ? 'powershell' : '/bin/sh';
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the file extension for hook scripts
|
||||
*/
|
||||
getHookExtension: (): string => {
|
||||
return '.js'; // Both platforms can execute Node.js scripts
|
||||
},
|
||||
|
||||
/**
|
||||
* Finds the path to an executable command
|
||||
* @param name - Name of the executable to find
|
||||
* @returns Full path to the executable
|
||||
*/
|
||||
findExecutable: (name: string): string => {
|
||||
const cmd = isWindows ? `where ${name}` : `which ${name}`;
|
||||
return execSync(cmd, {
|
||||
encoding: 'utf8',
|
||||
stdio: ['ignore', 'pipe', 'ignore']
|
||||
}).trim();
|
||||
},
|
||||
|
||||
/**
|
||||
* Makes a file executable (Unix only - no-op on Windows)
|
||||
* @param path - Path to the file to make executable
|
||||
*/
|
||||
makeExecutable: (path: string): void => {
|
||||
if (!isWindows) {
|
||||
chmodSync(path, 0o755);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Installs uv package manager using platform-specific method
|
||||
*/
|
||||
installUv: (): void => {
|
||||
if (isWindows) {
|
||||
execSync('powershell -Command "irm https://astral.sh/uv/install.ps1 | iex"', {
|
||||
stdio: 'pipe'
|
||||
});
|
||||
} else {
|
||||
execSync('curl -LsSf https://astral.sh/uv/install.sh | sh', {
|
||||
stdio: 'pipe',
|
||||
shell: '/bin/sh'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns shell configuration file paths for the current platform
|
||||
* @returns Array of shell config file paths
|
||||
*/
|
||||
getShellConfigPaths: (): string[] => {
|
||||
const home = homedir();
|
||||
|
||||
if (isWindows) {
|
||||
return [
|
||||
join(home, 'Documents', 'PowerShell', 'Microsoft.PowerShell_profile.ps1'),
|
||||
join(home, 'Documents', 'WindowsPowerShell', 'Microsoft.PowerShell_profile.ps1')
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
join(home, '.bashrc'),
|
||||
join(home, '.zshrc'),
|
||||
join(home, '.bash_profile')
|
||||
];
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the appropriate alias syntax for the current platform's shell
|
||||
* @param aliasName - Name of the alias
|
||||
* @param command - Command to alias
|
||||
* @returns Alias definition string
|
||||
*/
|
||||
getAliasDefinition: (aliasName: string, command: string): string => {
|
||||
if (isWindows) {
|
||||
// PowerShell function syntax
|
||||
return `function ${aliasName} { ${command} $args }`;
|
||||
}
|
||||
|
||||
// Bash/Zsh alias syntax
|
||||
return `alias ${aliasName}='${command}'`;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns whether the current platform is Windows
|
||||
*/
|
||||
isWindows: (): boolean => isWindows,
|
||||
|
||||
/**
|
||||
* Returns whether the current platform is Unix-like (macOS/Linux)
|
||||
*/
|
||||
isUnix: (): boolean => !isWindows
|
||||
};
|
||||
Reference in New Issue
Block a user