tlc-claude-code 1.8.5 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/tlc/bootstrap.md +77 -0
- package/.claude/commands/tlc/build.md +20 -6
- package/.claude/commands/tlc/deploy.md +194 -2
- package/.claude/commands/tlc/e2e-verify.md +214 -0
- package/.claude/commands/tlc/guard.md +191 -0
- package/.claude/commands/tlc/help.md +32 -0
- package/.claude/commands/tlc/init.md +73 -37
- package/.claude/commands/tlc/llm.md +19 -4
- package/.claude/commands/tlc/preflight.md +134 -0
- package/.claude/commands/tlc/recall.md +87 -0
- package/.claude/commands/tlc/remember.md +71 -0
- package/.claude/commands/tlc/review.md +17 -4
- package/.claude/commands/tlc/watchci.md +159 -0
- package/.claude/hooks/tlc-block-tools.sh +41 -0
- package/.claude/hooks/tlc-capture-exchange.sh +50 -0
- package/.claude/hooks/tlc-post-build.sh +38 -0
- package/.claude/hooks/tlc-post-push.sh +22 -0
- package/.claude/hooks/tlc-prompt-guard.sh +69 -0
- package/.claude/hooks/tlc-session-init.sh +123 -0
- package/CLAUDE.md +96 -201
- package/bin/install.js +171 -2
- package/bin/postinstall.js +45 -26
- package/dashboard-web/dist/assets/index-CdS5CHqu.css +1 -0
- package/dashboard-web/dist/assets/index-CwNPPVpg.js +483 -0
- package/dashboard-web/dist/assets/index-CwNPPVpg.js.map +1 -0
- package/dashboard-web/dist/index.html +2 -2
- package/docker-compose.dev.yml +18 -12
- package/package.json +3 -1
- package/server/index.js +240 -1
- package/server/lib/bug-writer.js +204 -0
- package/server/lib/bug-writer.test.js +279 -0
- package/server/lib/capture-bridge.js +242 -0
- package/server/lib/capture-bridge.test.js +363 -0
- package/server/lib/capture-guard.js +140 -0
- package/server/lib/capture-guard.test.js +182 -0
- package/server/lib/claude-cascade.js +247 -0
- package/server/lib/claude-cascade.test.js +245 -0
- package/server/lib/command-runner.js +159 -0
- package/server/lib/command-runner.test.js +92 -0
- package/server/lib/context-injection.js +121 -0
- package/server/lib/context-injection.test.js +340 -0
- package/server/lib/conversation-chunker.js +320 -0
- package/server/lib/conversation-chunker.test.js +573 -0
- package/server/lib/deploy/runners/dependency-runner.js +106 -0
- package/server/lib/deploy/runners/dependency-runner.test.js +148 -0
- package/server/lib/deploy/runners/secrets-runner.js +174 -0
- package/server/lib/deploy/runners/secrets-runner.test.js +127 -0
- package/server/lib/deploy/security-gates.js +11 -24
- package/server/lib/deploy/security-gates.test.js +9 -2
- package/server/lib/deploy-engine.js +182 -0
- package/server/lib/deploy-engine.test.js +147 -0
- package/server/lib/docker-api.js +137 -0
- package/server/lib/docker-api.test.js +202 -0
- package/server/lib/docker-client.js +297 -0
- package/server/lib/docker-client.test.js +308 -0
- package/server/lib/embedding-client.js +160 -0
- package/server/lib/embedding-client.test.js +243 -0
- package/server/lib/global-config.js +198 -0
- package/server/lib/global-config.test.js +288 -0
- package/server/lib/inherited-search.js +184 -0
- package/server/lib/inherited-search.test.js +343 -0
- package/server/lib/input-sanitizer.js +86 -0
- package/server/lib/input-sanitizer.test.js +117 -0
- package/server/lib/launchd-agent.js +225 -0
- package/server/lib/launchd-agent.test.js +185 -0
- package/server/lib/memory-api.js +182 -0
- package/server/lib/memory-api.test.js +320 -0
- package/server/lib/memory-bridge-e2e.test.js +160 -0
- package/server/lib/memory-committer.js +18 -4
- package/server/lib/memory-committer.test.js +21 -0
- package/server/lib/memory-hooks-capture.test.js +415 -0
- package/server/lib/memory-hooks-integration.test.js +98 -0
- package/server/lib/memory-hooks.js +139 -0
- package/server/lib/memory-inheritance.js +179 -0
- package/server/lib/memory-inheritance.test.js +360 -0
- package/server/lib/memory-store-adapter.js +105 -0
- package/server/lib/memory-store-adapter.test.js +141 -0
- package/server/lib/memory-wiring-e2e.test.js +93 -0
- package/server/lib/nginx-config.js +114 -0
- package/server/lib/nginx-config.test.js +82 -0
- package/server/lib/ollama-health.js +91 -0
- package/server/lib/ollama-health.test.js +74 -0
- package/server/lib/plan-writer.js +196 -0
- package/server/lib/plan-writer.test.js +298 -0
- package/server/lib/port-guard.js +44 -0
- package/server/lib/port-guard.test.js +65 -0
- package/server/lib/project-scanner.js +302 -0
- package/server/lib/project-scanner.test.js +541 -0
- package/server/lib/project-status.js +302 -0
- package/server/lib/project-status.test.js +470 -0
- package/server/lib/projects-registry.js +237 -0
- package/server/lib/projects-registry.test.js +275 -0
- package/server/lib/recall-command.js +207 -0
- package/server/lib/recall-command.test.js +306 -0
- package/server/lib/remember-command.js +98 -0
- package/server/lib/remember-command.test.js +288 -0
- package/server/lib/rich-capture.js +221 -0
- package/server/lib/rich-capture.test.js +312 -0
- package/server/lib/roadmap-api.js +200 -0
- package/server/lib/roadmap-api.test.js +318 -0
- package/server/lib/security/crypto-utils.test.js +2 -2
- package/server/lib/semantic-recall.js +242 -0
- package/server/lib/semantic-recall.test.js +463 -0
- package/server/lib/setup-generator.js +315 -0
- package/server/lib/setup-generator.test.js +303 -0
- package/server/lib/ssh-client.js +184 -0
- package/server/lib/ssh-client.test.js +127 -0
- package/server/lib/test-inventory.js +112 -0
- package/server/lib/test-inventory.test.js +360 -0
- package/server/lib/vector-indexer.js +246 -0
- package/server/lib/vector-indexer.test.js +459 -0
- package/server/lib/vector-store.js +260 -0
- package/server/lib/vector-store.test.js +706 -0
- package/server/lib/vps-api.js +184 -0
- package/server/lib/vps-api.test.js +208 -0
- package/server/lib/vps-bootstrap.js +124 -0
- package/server/lib/vps-bootstrap.test.js +79 -0
- package/server/lib/vps-monitor.js +126 -0
- package/server/lib/vps-monitor.test.js +98 -0
- package/server/lib/workspace-api.js +992 -0
- package/server/lib/workspace-api.test.js +1217 -0
- package/server/lib/workspace-bootstrap.js +164 -0
- package/server/lib/workspace-bootstrap.test.js +503 -0
- package/server/lib/workspace-context.js +129 -0
- package/server/lib/workspace-context.test.js +214 -0
- package/server/lib/workspace-detector.js +162 -0
- package/server/lib/workspace-detector.test.js +193 -0
- package/server/lib/workspace-init.js +307 -0
- package/server/lib/workspace-init.test.js +244 -0
- package/server/lib/workspace-snapshot.js +236 -0
- package/server/lib/workspace-snapshot.test.js +444 -0
- package/server/lib/workspace-watcher.js +162 -0
- package/server/lib/workspace-watcher.test.js +257 -0
- package/server/package-lock.json +1306 -17
- package/server/package.json +7 -0
- package/dashboard-web/dist/assets/index-B1I_joSL.js +0 -393
- package/dashboard-web/dist/assets/index-B1I_joSL.js.map +0 -1
- package/dashboard-web/dist/assets/index-Trhg1C1Y.css +0 -1
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LaunchAgent plist generator for macOS.
|
|
3
|
+
*
|
|
4
|
+
* Generates, installs, and manages a macOS LaunchAgent that keeps
|
|
5
|
+
* the TLC server running permanently — auto-starts on login,
|
|
6
|
+
* auto-restarts on crash.
|
|
7
|
+
*
|
|
8
|
+
* @module launchd-agent
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const os = require('os');
|
|
14
|
+
|
|
15
|
+
/** LaunchAgent label */
|
|
16
|
+
const PLIST_LABEL = 'com.tlc.server';
|
|
17
|
+
|
|
18
|
+
/** Default plist install location */
|
|
19
|
+
const PLIST_PATH = path.join(os.homedir(), 'Library', 'LaunchAgents', `${PLIST_LABEL}.plist`);
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Generate a macOS LaunchAgent plist XML string.
|
|
23
|
+
*
|
|
24
|
+
* @param {object} opts
|
|
25
|
+
* @param {string} opts.projectRoot - Absolute path to the TLC project
|
|
26
|
+
* @param {number} [opts.port=3147] - Server port
|
|
27
|
+
* @returns {string} Valid XML plist
|
|
28
|
+
*/
|
|
29
|
+
function generatePlist(opts) {
|
|
30
|
+
const { projectRoot, port = 3147 } = opts;
|
|
31
|
+
const nodePath = process.execPath;
|
|
32
|
+
const serverScript = path.join(projectRoot, 'server', 'index.js');
|
|
33
|
+
const logDir = path.join(os.homedir(), '.tlc', 'logs');
|
|
34
|
+
const logFile = path.join(logDir, 'server.log');
|
|
35
|
+
const envPath = process.env.PATH || '/usr/local/bin:/usr/bin:/bin';
|
|
36
|
+
|
|
37
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
38
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
39
|
+
<plist version="1.0">
|
|
40
|
+
<dict>
|
|
41
|
+
<key>Label</key>
|
|
42
|
+
<string>${PLIST_LABEL}</string>
|
|
43
|
+
|
|
44
|
+
<key>ProgramArguments</key>
|
|
45
|
+
<array>
|
|
46
|
+
<string>${nodePath}</string>
|
|
47
|
+
<string>${serverScript}</string>
|
|
48
|
+
</array>
|
|
49
|
+
|
|
50
|
+
<key>WorkingDirectory</key>
|
|
51
|
+
<string>${projectRoot}</string>
|
|
52
|
+
|
|
53
|
+
<key>EnvironmentVariables</key>
|
|
54
|
+
<dict>
|
|
55
|
+
<key>PATH</key>
|
|
56
|
+
<string>${envPath}</string>
|
|
57
|
+
<key>HOME</key>
|
|
58
|
+
<string>${os.homedir()}</string>
|
|
59
|
+
<key>NODE_ENV</key>
|
|
60
|
+
<string>development</string>
|
|
61
|
+
<key>TLC_PORT</key>
|
|
62
|
+
<string>${port}</string>
|
|
63
|
+
</dict>
|
|
64
|
+
|
|
65
|
+
<key>KeepAlive</key>
|
|
66
|
+
<true/>
|
|
67
|
+
|
|
68
|
+
<key>ThrottleInterval</key>
|
|
69
|
+
<integer>10</integer>
|
|
70
|
+
|
|
71
|
+
<key>StandardOutPath</key>
|
|
72
|
+
<string>${logFile}</string>
|
|
73
|
+
|
|
74
|
+
<key>StandardErrorPath</key>
|
|
75
|
+
<string>${logFile}</string>
|
|
76
|
+
</dict>
|
|
77
|
+
</plist>
|
|
78
|
+
`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Write the plist file to disk.
|
|
83
|
+
*
|
|
84
|
+
* @param {object} opts
|
|
85
|
+
* @param {string} opts.projectRoot - Absolute path to the TLC project
|
|
86
|
+
* @param {string} [opts.targetDir] - Override install directory (for testing)
|
|
87
|
+
* @param {number} [opts.port=3147] - Server port
|
|
88
|
+
*/
|
|
89
|
+
function installAgent(opts) {
|
|
90
|
+
const targetDir = opts.targetDir || path.join(os.homedir(), 'Library', 'LaunchAgents');
|
|
91
|
+
const plistPath = path.join(targetDir, `${PLIST_LABEL}.plist`);
|
|
92
|
+
|
|
93
|
+
// Ensure log directory exists
|
|
94
|
+
const logDir = path.join(os.homedir(), '.tlc', 'logs');
|
|
95
|
+
if (!fs.existsSync(logDir)) {
|
|
96
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Ensure target directory exists
|
|
100
|
+
if (!fs.existsSync(targetDir)) {
|
|
101
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const xml = generatePlist(opts);
|
|
105
|
+
fs.writeFileSync(plistPath, xml, 'utf-8');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Remove the plist file from disk.
|
|
110
|
+
*
|
|
111
|
+
* @param {object} [opts]
|
|
112
|
+
* @param {string} [opts.targetDir] - Override install directory (for testing)
|
|
113
|
+
*/
|
|
114
|
+
function uninstallAgent(opts = {}) {
|
|
115
|
+
const targetDir = opts.targetDir || path.join(os.homedir(), 'Library', 'LaunchAgents');
|
|
116
|
+
const plistPath = path.join(targetDir, `${PLIST_LABEL}.plist`);
|
|
117
|
+
|
|
118
|
+
if (fs.existsSync(plistPath)) {
|
|
119
|
+
fs.unlinkSync(plistPath);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Check if the plist file is installed.
|
|
125
|
+
*
|
|
126
|
+
* @param {object} [opts]
|
|
127
|
+
* @param {string} [opts.targetDir] - Override install directory (for testing)
|
|
128
|
+
* @returns {boolean}
|
|
129
|
+
*/
|
|
130
|
+
function isInstalled(opts = {}) {
|
|
131
|
+
const targetDir = opts.targetDir || path.join(os.homedir(), 'Library', 'LaunchAgents');
|
|
132
|
+
const plistPath = path.join(targetDir, `${PLIST_LABEL}.plist`);
|
|
133
|
+
return fs.existsSync(plistPath);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Load the agent into launchd.
|
|
138
|
+
*
|
|
139
|
+
* @param {object} [opts]
|
|
140
|
+
* @param {Function} [opts.exec] - Async exec function (for testing)
|
|
141
|
+
* @param {string} [opts.targetDir] - Override install directory
|
|
142
|
+
* @returns {Promise<{ok: boolean, error?: string}>}
|
|
143
|
+
*/
|
|
144
|
+
async function loadAgent(opts = {}) {
|
|
145
|
+
const exec = opts.exec || defaultExec;
|
|
146
|
+
const targetDir = opts.targetDir || path.join(os.homedir(), 'Library', 'LaunchAgents');
|
|
147
|
+
const plistPath = path.join(targetDir, `${PLIST_LABEL}.plist`);
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
await exec(`launchctl load -w ${plistPath}`);
|
|
151
|
+
return { ok: true };
|
|
152
|
+
} catch (err) {
|
|
153
|
+
return { ok: false, error: err.message };
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Unload the agent from launchd and remove the plist.
|
|
159
|
+
*
|
|
160
|
+
* @param {object} [opts]
|
|
161
|
+
* @param {Function} [opts.exec] - Async exec function (for testing)
|
|
162
|
+
* @param {string} [opts.targetDir] - Override install directory
|
|
163
|
+
* @returns {Promise<{ok: boolean, error?: string}>}
|
|
164
|
+
*/
|
|
165
|
+
async function unloadAgent(opts = {}) {
|
|
166
|
+
const exec = opts.exec || defaultExec;
|
|
167
|
+
const targetDir = opts.targetDir || path.join(os.homedir(), 'Library', 'LaunchAgents');
|
|
168
|
+
const plistPath = path.join(targetDir, `${PLIST_LABEL}.plist`);
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
await exec(`launchctl unload ${plistPath}`);
|
|
172
|
+
} catch {
|
|
173
|
+
// Agent may not be loaded — that's fine
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
uninstallAgent({ targetDir });
|
|
177
|
+
return { ok: true };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Check if the agent is loaded in launchd.
|
|
182
|
+
*
|
|
183
|
+
* @param {object} [opts]
|
|
184
|
+
* @param {Function} [opts.exec] - Async exec function (for testing)
|
|
185
|
+
* @returns {Promise<{loaded: boolean, pid?: number}>}
|
|
186
|
+
*/
|
|
187
|
+
async function statusAgent(opts = {}) {
|
|
188
|
+
const exec = opts.exec || defaultExec;
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
const { stdout } = await exec(`launchctl list ${PLIST_LABEL}`);
|
|
192
|
+
const parts = stdout.trim().split('\t');
|
|
193
|
+
const pid = parts[0] && parts[0] !== '-' ? parseInt(parts[0], 10) : null;
|
|
194
|
+
return { loaded: true, pid };
|
|
195
|
+
} catch {
|
|
196
|
+
return { loaded: false };
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Default exec using child_process.
|
|
202
|
+
* @param {string} cmd
|
|
203
|
+
* @returns {Promise<{stdout: string, stderr: string}>}
|
|
204
|
+
*/
|
|
205
|
+
function defaultExec(cmd) {
|
|
206
|
+
const { exec } = require('child_process');
|
|
207
|
+
return new Promise((resolve, reject) => {
|
|
208
|
+
exec(cmd, (err, stdout, stderr) => {
|
|
209
|
+
if (err) return reject(err);
|
|
210
|
+
resolve({ stdout, stderr });
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
module.exports = {
|
|
216
|
+
generatePlist,
|
|
217
|
+
installAgent,
|
|
218
|
+
uninstallAgent,
|
|
219
|
+
isInstalled,
|
|
220
|
+
loadAgent,
|
|
221
|
+
unloadAgent,
|
|
222
|
+
statusAgent,
|
|
223
|
+
PLIST_LABEL,
|
|
224
|
+
PLIST_PATH,
|
|
225
|
+
};
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LaunchAgent plist generator tests - Phase 83 Task 1
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import os from 'os';
|
|
8
|
+
import fs from 'fs';
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
generatePlist,
|
|
12
|
+
installAgent,
|
|
13
|
+
uninstallAgent,
|
|
14
|
+
isInstalled,
|
|
15
|
+
loadAgent,
|
|
16
|
+
unloadAgent,
|
|
17
|
+
statusAgent,
|
|
18
|
+
PLIST_LABEL,
|
|
19
|
+
PLIST_PATH,
|
|
20
|
+
} from './launchd-agent.js';
|
|
21
|
+
|
|
22
|
+
describe('launchd-agent', () => {
|
|
23
|
+
describe('generatePlist', () => {
|
|
24
|
+
it('returns valid XML with correct label', () => {
|
|
25
|
+
const xml = generatePlist({ projectRoot: '/Users/dev/myproject' });
|
|
26
|
+
expect(xml).toContain('<?xml version="1.0"');
|
|
27
|
+
expect(xml).toContain('<!DOCTYPE plist');
|
|
28
|
+
expect(xml).toContain('<string>com.tlc.server</string>');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('includes KeepAlive and ThrottleInterval', () => {
|
|
32
|
+
const xml = generatePlist({ projectRoot: '/Users/dev/myproject' });
|
|
33
|
+
expect(xml).toContain('<key>KeepAlive</key>');
|
|
34
|
+
expect(xml).toContain('<true/>');
|
|
35
|
+
expect(xml).toContain('<key>ThrottleInterval</key>');
|
|
36
|
+
expect(xml).toContain('<integer>10</integer>');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('sets WorkingDirectory to project root', () => {
|
|
40
|
+
const xml = generatePlist({ projectRoot: '/Users/dev/myproject' });
|
|
41
|
+
expect(xml).toContain('<key>WorkingDirectory</key>');
|
|
42
|
+
expect(xml).toContain('<string>/Users/dev/myproject</string>');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('sets EnvironmentVariables including PATH and NODE_ENV', () => {
|
|
46
|
+
const xml = generatePlist({ projectRoot: '/Users/dev/myproject' });
|
|
47
|
+
expect(xml).toContain('<key>EnvironmentVariables</key>');
|
|
48
|
+
expect(xml).toContain('<key>NODE_ENV</key>');
|
|
49
|
+
expect(xml).toContain('<string>development</string>');
|
|
50
|
+
expect(xml).toContain('<key>PATH</key>');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('uses absolute node path in ProgramArguments', () => {
|
|
54
|
+
const xml = generatePlist({ projectRoot: '/Users/dev/myproject' });
|
|
55
|
+
expect(xml).toContain('<key>ProgramArguments</key>');
|
|
56
|
+
// Should contain path to node and path to server/index.js
|
|
57
|
+
expect(xml).toContain('server/index.js');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('sets log paths under ~/.tlc/logs/', () => {
|
|
61
|
+
const xml = generatePlist({ projectRoot: '/Users/dev/myproject' });
|
|
62
|
+
expect(xml).toContain('<key>StandardOutPath</key>');
|
|
63
|
+
expect(xml).toContain('<key>StandardErrorPath</key>');
|
|
64
|
+
expect(xml).toContain('.tlc/logs/server.log');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('allows custom port via opts', () => {
|
|
68
|
+
const xml = generatePlist({ projectRoot: '/Users/dev/myproject', port: 4000 });
|
|
69
|
+
expect(xml).toContain('<key>TLC_PORT</key>');
|
|
70
|
+
expect(xml).toContain('<string>4000</string>');
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe('installAgent', () => {
|
|
75
|
+
let tmpDir;
|
|
76
|
+
|
|
77
|
+
beforeEach(() => {
|
|
78
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tlc-launchd-'));
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
afterEach(() => {
|
|
82
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('writes plist file to target directory', () => {
|
|
86
|
+
const plistPath = path.join(tmpDir, 'com.tlc.server.plist');
|
|
87
|
+
installAgent({ projectRoot: '/Users/dev/myproject', targetDir: tmpDir });
|
|
88
|
+
expect(fs.existsSync(plistPath)).toBe(true);
|
|
89
|
+
const content = fs.readFileSync(plistPath, 'utf-8');
|
|
90
|
+
expect(content).toContain('com.tlc.server');
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe('uninstallAgent', () => {
|
|
95
|
+
let tmpDir;
|
|
96
|
+
|
|
97
|
+
beforeEach(() => {
|
|
98
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tlc-launchd-'));
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
afterEach(() => {
|
|
102
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('removes plist file', () => {
|
|
106
|
+
const plistPath = path.join(tmpDir, 'com.tlc.server.plist');
|
|
107
|
+
fs.writeFileSync(plistPath, '<plist/>');
|
|
108
|
+
uninstallAgent({ targetDir: tmpDir });
|
|
109
|
+
expect(fs.existsSync(plistPath)).toBe(false);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe('isInstalled', () => {
|
|
114
|
+
let tmpDir;
|
|
115
|
+
|
|
116
|
+
beforeEach(() => {
|
|
117
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tlc-launchd-'));
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
afterEach(() => {
|
|
121
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('returns true when plist exists', () => {
|
|
125
|
+
fs.writeFileSync(path.join(tmpDir, 'com.tlc.server.plist'), '<plist/>');
|
|
126
|
+
expect(isInstalled({ targetDir: tmpDir })).toBe(true);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('returns false when plist does not exist', () => {
|
|
130
|
+
expect(isInstalled({ targetDir: tmpDir })).toBe(false);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe('loadAgent', () => {
|
|
135
|
+
it('calls launchctl load with correct path', async () => {
|
|
136
|
+
const mockExec = vi.fn().mockResolvedValue({ stdout: '', stderr: '' });
|
|
137
|
+
const result = await loadAgent({ exec: mockExec, targetDir: '/tmp/agents' });
|
|
138
|
+
expect(mockExec).toHaveBeenCalledWith(
|
|
139
|
+
expect.stringContaining('launchctl load'),
|
|
140
|
+
);
|
|
141
|
+
expect(mockExec).toHaveBeenCalledWith(
|
|
142
|
+
expect.stringContaining('com.tlc.server.plist'),
|
|
143
|
+
);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe('unloadAgent', () => {
|
|
148
|
+
it('calls launchctl unload then removes plist', async () => {
|
|
149
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tlc-launchd-'));
|
|
150
|
+
const plistPath = path.join(tmpDir, 'com.tlc.server.plist');
|
|
151
|
+
fs.writeFileSync(plistPath, '<plist/>');
|
|
152
|
+
|
|
153
|
+
const mockExec = vi.fn().mockResolvedValue({ stdout: '', stderr: '' });
|
|
154
|
+
await unloadAgent({ exec: mockExec, targetDir: tmpDir });
|
|
155
|
+
|
|
156
|
+
expect(mockExec).toHaveBeenCalledWith(
|
|
157
|
+
expect.stringContaining('launchctl unload'),
|
|
158
|
+
);
|
|
159
|
+
expect(fs.existsSync(plistPath)).toBe(false);
|
|
160
|
+
|
|
161
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
describe('statusAgent', () => {
|
|
166
|
+
it('returns loaded when launchctl finds the agent', async () => {
|
|
167
|
+
const mockExec = vi.fn().mockResolvedValue({ stdout: '12345\t0\tcom.tlc.server', stderr: '' });
|
|
168
|
+
const result = await statusAgent({ exec: mockExec });
|
|
169
|
+
expect(result.loaded).toBe(true);
|
|
170
|
+
expect(result.pid).toBeDefined();
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('returns not loaded when launchctl does not find agent', async () => {
|
|
174
|
+
const mockExec = vi.fn().mockRejectedValue(new Error('Could not find service'));
|
|
175
|
+
const result = await statusAgent({ exec: mockExec });
|
|
176
|
+
expect(result.loaded).toBe(false);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
describe('constants', () => {
|
|
181
|
+
it('exports correct plist label', () => {
|
|
182
|
+
expect(PLIST_LABEL).toBe('com.tlc.server');
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
});
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory API — HTTP handler factory for memory-related endpoints.
|
|
3
|
+
*
|
|
4
|
+
* Factory function `createMemoryApi` accepts injected dependencies and returns
|
|
5
|
+
* handler methods that accept Express-compatible (req, res) objects.
|
|
6
|
+
*
|
|
7
|
+
* Endpoints provided:
|
|
8
|
+
* - handleSearch — semantic search across memory
|
|
9
|
+
* - handleListConversations — paginated conversation list
|
|
10
|
+
* - handleGetConversation — single conversation detail
|
|
11
|
+
* - handleListDecisions — list all decisions
|
|
12
|
+
* - handleListGotchas — list all gotchas
|
|
13
|
+
* - handleGetStats — vector DB statistics
|
|
14
|
+
* - handleRebuild — trigger vector index rebuild
|
|
15
|
+
* - handleRemember — store permanent memory
|
|
16
|
+
*
|
|
17
|
+
* @module memory-api
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Create a memory API handler instance.
|
|
22
|
+
*
|
|
23
|
+
* @param {object} deps
|
|
24
|
+
* @param {object} deps.semanticRecall - Semantic recall with recall(query, context, options)
|
|
25
|
+
* @param {object} deps.vectorIndexer - Vector indexer with indexAll(projectRoot)
|
|
26
|
+
* @param {object} deps.richCapture - Rich capture with processChunk(text, metadata)
|
|
27
|
+
* @param {object} deps.embeddingClient - Embedding client with embed(text)
|
|
28
|
+
* @param {object} deps.memoryStore - Memory store with list/get methods
|
|
29
|
+
* @returns {object} Handler methods for each memory endpoint
|
|
30
|
+
*/
|
|
31
|
+
function createMemoryApi({ semanticRecall, vectorIndexer, richCapture, embeddingClient, memoryStore }) {
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Search memory semantically.
|
|
35
|
+
*
|
|
36
|
+
* Query params: q (search query), scope (project|workspace|global)
|
|
37
|
+
*
|
|
38
|
+
* @param {object} req - Express request
|
|
39
|
+
* @param {object} res - Express response
|
|
40
|
+
*/
|
|
41
|
+
async function handleSearch(req, res) {
|
|
42
|
+
const { q = '', scope } = req.query;
|
|
43
|
+
|
|
44
|
+
if (!q) {
|
|
45
|
+
return res.json({ results: [] });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const context = {};
|
|
49
|
+
const options = {};
|
|
50
|
+
if (scope) {
|
|
51
|
+
options.scope = scope;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const results = await semanticRecall.recall(q, context, options);
|
|
55
|
+
res.json({ results });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* List conversations with pagination.
|
|
60
|
+
*
|
|
61
|
+
* Query params: page, limit, project
|
|
62
|
+
*
|
|
63
|
+
* @param {object} req - Express request
|
|
64
|
+
* @param {object} res - Express response
|
|
65
|
+
*/
|
|
66
|
+
async function handleListConversations(req, res) {
|
|
67
|
+
const page = parseInt(req.query.page, 10) || 1;
|
|
68
|
+
const limit = parseInt(req.query.limit, 10) || 20;
|
|
69
|
+
const { project } = req.query;
|
|
70
|
+
|
|
71
|
+
const options = { page, limit };
|
|
72
|
+
if (project) {
|
|
73
|
+
options.project = project;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const result = await memoryStore.listConversations(options);
|
|
77
|
+
res.json({ items: result.items, total: result.total });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Get a single conversation by ID.
|
|
82
|
+
*
|
|
83
|
+
* @param {object} req - Express request with params.id
|
|
84
|
+
* @param {object} res - Express response
|
|
85
|
+
*/
|
|
86
|
+
async function handleGetConversation(req, res) {
|
|
87
|
+
const { id } = req.params;
|
|
88
|
+
const conversation = await memoryStore.getConversation(id);
|
|
89
|
+
|
|
90
|
+
if (!conversation) {
|
|
91
|
+
return res.status(404).json({ error: 'Conversation not found' });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
res.json(conversation);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* List all decisions, optionally filtered by project.
|
|
99
|
+
*
|
|
100
|
+
* @param {object} req - Express request
|
|
101
|
+
* @param {object} res - Express response
|
|
102
|
+
*/
|
|
103
|
+
async function handleListDecisions(req, res) {
|
|
104
|
+
const { project } = req.query;
|
|
105
|
+
const options = {};
|
|
106
|
+
if (project) {
|
|
107
|
+
options.project = project;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const decisions = await memoryStore.listDecisions(options);
|
|
111
|
+
res.json({ decisions });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* List all gotchas, optionally filtered by project.
|
|
116
|
+
*
|
|
117
|
+
* @param {object} req - Express request
|
|
118
|
+
* @param {object} res - Express response
|
|
119
|
+
*/
|
|
120
|
+
async function handleListGotchas(req, res) {
|
|
121
|
+
const { project } = req.query;
|
|
122
|
+
const options = {};
|
|
123
|
+
if (project) {
|
|
124
|
+
options.project = project;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const gotchas = await memoryStore.listGotchas(options);
|
|
128
|
+
res.json({ gotchas });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Get vector DB statistics.
|
|
133
|
+
*
|
|
134
|
+
* @param {object} req - Express request
|
|
135
|
+
* @param {object} res - Express response
|
|
136
|
+
*/
|
|
137
|
+
async function handleGetStats(req, res) {
|
|
138
|
+
const stats = await memoryStore.getStats();
|
|
139
|
+
res.json(stats);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Trigger a full vector index rebuild.
|
|
144
|
+
*
|
|
145
|
+
* Body: { projectRoot }
|
|
146
|
+
*
|
|
147
|
+
* @param {object} req - Express request
|
|
148
|
+
* @param {object} res - Express response
|
|
149
|
+
*/
|
|
150
|
+
async function handleRebuild(req, res) {
|
|
151
|
+
const { projectRoot } = req.body;
|
|
152
|
+
const result = await vectorIndexer.indexAll(projectRoot);
|
|
153
|
+
res.json(result);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Store a permanent memory entry.
|
|
158
|
+
*
|
|
159
|
+
* Body: { text, metadata }
|
|
160
|
+
*
|
|
161
|
+
* @param {object} req - Express request
|
|
162
|
+
* @param {object} res - Express response
|
|
163
|
+
*/
|
|
164
|
+
async function handleRemember(req, res) {
|
|
165
|
+
const { text, metadata = {} } = req.body;
|
|
166
|
+
const result = await richCapture.processChunk(text, { ...metadata, permanent: true });
|
|
167
|
+
res.json(result);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
handleSearch,
|
|
172
|
+
handleListConversations,
|
|
173
|
+
handleGetConversation,
|
|
174
|
+
handleListDecisions,
|
|
175
|
+
handleListGotchas,
|
|
176
|
+
handleGetStats,
|
|
177
|
+
handleRebuild,
|
|
178
|
+
handleRemember,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
module.exports = { createMemoryApi };
|