clementine-agent 1.2.0 → 1.2.1
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/dist/cli/dashboard.js +49 -1
- package/dist/cli/index.js +27 -8
- package/dist/config/keychain-first-run-wizard.d.ts +16 -5
- package/dist/config/keychain-first-run-wizard.js +93 -14
- package/dist/config/keychain-fix-acl.d.ts +10 -2
- package/dist/config/keychain-fix-acl.js +21 -7
- package/dist/memory/seed-user-model.d.ts +38 -0
- package/dist/memory/seed-user-model.js +174 -0
- package/package.json +1 -1
package/dist/cli/dashboard.js
CHANGED
|
@@ -4470,6 +4470,52 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
|
|
|
4470
4470
|
res.status(500).json({ error: String(err) });
|
|
4471
4471
|
}
|
|
4472
4472
|
});
|
|
4473
|
+
// Seed the user model from existing memory — one-shot Haiku pass over
|
|
4474
|
+
// MEMORY.md, top-salience chunks, and recent session summaries. Returns
|
|
4475
|
+
// proposed values for each slot; the dashboard UI shows them in an
|
|
4476
|
+
// editable review panel before applying. Nothing is written to
|
|
4477
|
+
// user_model_blocks until the user clicks "Apply" on a slot.
|
|
4478
|
+
app.post('/api/user-model/seed', async (_req, res) => {
|
|
4479
|
+
try {
|
|
4480
|
+
const gateway = await getGateway();
|
|
4481
|
+
const store = gateway.assistant?.memoryStore;
|
|
4482
|
+
if (!store) {
|
|
4483
|
+
res.status(503).json({ error: 'Memory store not available' });
|
|
4484
|
+
return;
|
|
4485
|
+
}
|
|
4486
|
+
// Build the LLM caller using the same Claude Agent SDK pattern as
|
|
4487
|
+
// periodic memory consolidation (see src/index.ts:684).
|
|
4488
|
+
const { query } = await import('@anthropic-ai/claude-agent-sdk');
|
|
4489
|
+
const llmCall = async (prompt) => {
|
|
4490
|
+
try {
|
|
4491
|
+
let result = '';
|
|
4492
|
+
const stream = query({
|
|
4493
|
+
prompt,
|
|
4494
|
+
options: {
|
|
4495
|
+
model: 'claude-haiku-4-5-20251001',
|
|
4496
|
+
maxTurns: 1,
|
|
4497
|
+
systemPrompt: 'You are a memory consolidation assistant. Extract only facts directly evidenced by the corpus. Be terse. Output exactly the requested format.',
|
|
4498
|
+
},
|
|
4499
|
+
});
|
|
4500
|
+
for await (const msg of stream) {
|
|
4501
|
+
if (msg.type === 'result') {
|
|
4502
|
+
result = msg.result ?? '';
|
|
4503
|
+
}
|
|
4504
|
+
}
|
|
4505
|
+
return result;
|
|
4506
|
+
}
|
|
4507
|
+
catch {
|
|
4508
|
+
return '';
|
|
4509
|
+
}
|
|
4510
|
+
};
|
|
4511
|
+
const { seedUserModelFromMemory } = await import('../memory/seed-user-model.js');
|
|
4512
|
+
const proposals = await seedUserModelFromMemory(store, llmCall);
|
|
4513
|
+
res.json({ ok: true, proposals });
|
|
4514
|
+
}
|
|
4515
|
+
catch (err) {
|
|
4516
|
+
res.status(500).json({ error: String(err) });
|
|
4517
|
+
}
|
|
4518
|
+
});
|
|
4473
4519
|
// ── Memory chunk CRUD (dashboard curation) ───────────────────────
|
|
4474
4520
|
// Lets the user fix wrong/stale memory directly from the search panel
|
|
4475
4521
|
// instead of having to wait for auto-extraction to drift in the right
|
|
@@ -10801,13 +10847,15 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
|
|
|
10801
10847
|
<div style="color:var(--muted,#888);margin-bottom:12px;font-size:13px">
|
|
10802
10848
|
What the agent always knows about you. These slots load into every conversation's context (above retrieved memory). Edit directly to correct or steer.
|
|
10803
10849
|
</div>
|
|
10804
|
-
<div style="display:flex;gap:8px;margin-bottom:12px;align-items:center">
|
|
10850
|
+
<div style="display:flex;gap:8px;margin-bottom:12px;align-items:center;flex-wrap:wrap">
|
|
10805
10851
|
<label style="font-size:13px;color:var(--text-secondary)">Scope:</label>
|
|
10806
10852
|
<select id="user-model-scope" style="padding:6px 10px;border:1px solid var(--border);border-radius:6px;background:var(--bg-input);color:var(--text);font-size:13px" onchange="loadUserModel()">
|
|
10807
10853
|
<option value="">Global</option>
|
|
10808
10854
|
</select>
|
|
10809
10855
|
<button class="btn" onclick="loadUserModel()" style="font-size:13px">Refresh</button>
|
|
10856
|
+
<button class="btn-primary" onclick="seedUserModel()" style="font-size:13px;margin-left:auto" title="One-shot Haiku pass over MEMORY.md + top-salience chunks + recent sessions to propose initial slot values. Review and apply per-slot.">✨ Seed from existing memory</button>
|
|
10810
10857
|
</div>
|
|
10858
|
+
<div id="user-model-seed-panel" style="display:none;margin-bottom:14px"></div>
|
|
10811
10859
|
<div id="user-model-panel"><div class="empty-state">Loading user model…</div></div>
|
|
10812
10860
|
</div>
|
|
10813
10861
|
|
package/dist/cli/index.js
CHANGED
|
@@ -1333,7 +1333,7 @@ async function cmdConfigHardenPermissions(opts) {
|
|
|
1333
1333
|
// ── Config keychain-fix-acl ─────────────────────────────────────────
|
|
1334
1334
|
async function cmdConfigKeychainFixAcl(opts) {
|
|
1335
1335
|
const { listClementineKeychainEntries, fixAllClementineEntries } = await import('../config/keychain-fix-acl.js');
|
|
1336
|
-
const { markKeychainWizardDone } = await import('../config/keychain-first-run-wizard.js');
|
|
1336
|
+
const { markKeychainWizardDone, readPasswordFromTty } = await import('../config/keychain-first-run-wizard.js');
|
|
1337
1337
|
const DIM = '\x1b[0;90m';
|
|
1338
1338
|
const BOLD = '\x1b[1m';
|
|
1339
1339
|
const GREEN = '\x1b[0;32m';
|
|
@@ -1358,35 +1358,54 @@ async function cmdConfigKeychainFixAcl(opts) {
|
|
|
1358
1358
|
console.log();
|
|
1359
1359
|
return;
|
|
1360
1360
|
}
|
|
1361
|
+
console.log(` ${DIM}Enter your macOS login password ONCE — it authorizes${RESET}`);
|
|
1362
|
+
console.log(` ${DIM}all ${entries.length} ACL update${entries.length === 1 ? '' : 's'} in a single pass. Not stored.${RESET}`);
|
|
1363
|
+
console.log();
|
|
1364
|
+
const password = await readPasswordFromTty(` ${BOLD}macOS login password:${RESET} `);
|
|
1365
|
+
if (!password) {
|
|
1366
|
+
console.log();
|
|
1367
|
+
console.log(` ${YELLOW}Empty password — aborted.${RESET}`);
|
|
1368
|
+
console.log();
|
|
1369
|
+
return;
|
|
1370
|
+
}
|
|
1371
|
+
console.log();
|
|
1361
1372
|
console.log(` ${BOLD}Fixing ACLs...${RESET}`);
|
|
1362
|
-
console.log(` ${DIM}macOS may ask for your login keychain password (the system prompt — it DOES appear).${RESET}`);
|
|
1363
|
-
console.log(` ${DIM}You may also be asked to "Always Allow" — pick that.${RESET}`);
|
|
1364
1373
|
console.log();
|
|
1365
|
-
const results = fixAllClementineEntries();
|
|
1374
|
+
const results = fixAllClementineEntries({ keychainPassword: password });
|
|
1366
1375
|
let okCount = 0;
|
|
1367
1376
|
let failCount = 0;
|
|
1377
|
+
let wrongPasswordHit = false;
|
|
1368
1378
|
for (const r of results) {
|
|
1369
1379
|
if (r.status === 'fixed') {
|
|
1370
1380
|
console.log(` ${GREEN}✓${RESET} ${r.account}`);
|
|
1371
1381
|
okCount++;
|
|
1372
1382
|
}
|
|
1373
|
-
else {
|
|
1383
|
+
else if (r.status === 'failed') {
|
|
1374
1384
|
console.log(` ${RED}✗${RESET} ${r.account} ${DIM}— ${r.error}${RESET}`);
|
|
1375
1385
|
failCount++;
|
|
1386
|
+
if (r.error && /MAC verification|AuthFailure|UserCanceled|-25293/i.test(r.error)) {
|
|
1387
|
+
wrongPasswordHit = true;
|
|
1388
|
+
}
|
|
1376
1389
|
}
|
|
1377
1390
|
}
|
|
1378
1391
|
console.log();
|
|
1379
1392
|
if (failCount === 0) {
|
|
1380
1393
|
console.log(` ${GREEN}All ${okCount} entries fixed.${RESET} ${DIM}Future reads via the security CLI succeed silently.${RESET}`);
|
|
1381
1394
|
}
|
|
1395
|
+
else if (wrongPasswordHit && okCount === 0) {
|
|
1396
|
+
console.log(` ${RED}Wrong password — no entries repaired.${RESET}`);
|
|
1397
|
+
console.log(` ${DIM}Re-run: clementine config keychain-fix-acl${RESET}`);
|
|
1398
|
+
}
|
|
1382
1399
|
else {
|
|
1383
1400
|
console.log(` ${YELLOW}${okCount} fixed, ${failCount} failed.${RESET}`);
|
|
1384
1401
|
console.log(` ${DIM}Failed entries can be fixed manually in Keychain Access.app:${RESET}`);
|
|
1385
1402
|
console.log(` ${DIM} search "clementine-agent" → double-click → Access Control → Allow all applications.${RESET}`);
|
|
1386
1403
|
}
|
|
1387
|
-
//
|
|
1388
|
-
// to deal with this via the manual command.
|
|
1389
|
-
|
|
1404
|
+
// Mark the launch wizard satisfied unless the password was clearly wrong —
|
|
1405
|
+
// user has explicitly decided to deal with this via the manual command.
|
|
1406
|
+
if (!(wrongPasswordHit && okCount === 0)) {
|
|
1407
|
+
markKeychainWizardDone(BASE_DIR);
|
|
1408
|
+
}
|
|
1390
1409
|
console.log();
|
|
1391
1410
|
}
|
|
1392
1411
|
// ── Analytics ────────────────────────────────────────────────────────
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* One-time interactive wizard that repairs ACLs on legacy clementine-agent
|
|
3
|
-
* keychain entries during `clementine launch`.
|
|
3
|
+
* keychain entries during `clementine launch` / `clementine update`.
|
|
4
4
|
*
|
|
5
5
|
* Why this exists: entries written before commit 88cfd99 used
|
|
6
6
|
* `add-generic-password -T ''` (no apps pre-approved), so every Clementine
|
|
@@ -9,10 +9,11 @@
|
|
|
9
9
|
* entries that need a one-time partition-list repair to stop the prompt
|
|
10
10
|
* cascade.
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
* the
|
|
14
|
-
*
|
|
15
|
-
*
|
|
12
|
+
* macOS's `set-generic-password-partition-list` requires the login keychain
|
|
13
|
+
* password to authorize the ACL change — once per entry. To avoid prompting
|
|
14
|
+
* the user N times for N entries, we ask once via masked stdin, then pass
|
|
15
|
+
* the password to each call via -k. End result: one password entry, all
|
|
16
|
+
* entries fixed in one pass.
|
|
16
17
|
*
|
|
17
18
|
* Skipped when:
|
|
18
19
|
* - non-darwin platform (no keychain),
|
|
@@ -22,5 +23,15 @@
|
|
|
22
23
|
*/
|
|
23
24
|
/** Write the sentinel so the wizard skips on subsequent launches. */
|
|
24
25
|
export declare function markKeychainWizardDone(baseDir: string): void;
|
|
26
|
+
/**
|
|
27
|
+
* Read a line from the TTY without echoing characters. Each keypress shows
|
|
28
|
+
* an asterisk so the user has visual feedback for length. Returns the
|
|
29
|
+
* collected string. Ctrl-C aborts the process.
|
|
30
|
+
*
|
|
31
|
+
* Uses raw mode directly because Node's readline always echoes input.
|
|
32
|
+
* Exported so the manual `clementine config keychain-fix-acl` command can
|
|
33
|
+
* use the same one-prompt UX.
|
|
34
|
+
*/
|
|
35
|
+
export declare function readPasswordFromTty(prompt: string): Promise<string>;
|
|
25
36
|
export declare function runFirstRunKeychainWizardIfNeeded(baseDir: string): Promise<void>;
|
|
26
37
|
//# sourceMappingURL=keychain-first-run-wizard.d.ts.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* One-time interactive wizard that repairs ACLs on legacy clementine-agent
|
|
3
|
-
* keychain entries during `clementine launch`.
|
|
3
|
+
* keychain entries during `clementine launch` / `clementine update`.
|
|
4
4
|
*
|
|
5
5
|
* Why this exists: entries written before commit 88cfd99 used
|
|
6
6
|
* `add-generic-password -T ''` (no apps pre-approved), so every Clementine
|
|
@@ -9,10 +9,11 @@
|
|
|
9
9
|
* entries that need a one-time partition-list repair to stop the prompt
|
|
10
10
|
* cascade.
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
* the
|
|
14
|
-
*
|
|
15
|
-
*
|
|
12
|
+
* macOS's `set-generic-password-partition-list` requires the login keychain
|
|
13
|
+
* password to authorize the ACL change — once per entry. To avoid prompting
|
|
14
|
+
* the user N times for N entries, we ask once via masked stdin, then pass
|
|
15
|
+
* the password to each call via -k. End result: one password entry, all
|
|
16
|
+
* entries fixed in one pass.
|
|
16
17
|
*
|
|
17
18
|
* Skipped when:
|
|
18
19
|
* - non-darwin platform (no keychain),
|
|
@@ -38,6 +39,64 @@ export function markKeychainWizardDone(baseDir) {
|
|
|
38
39
|
// next launch — annoying but not broken.
|
|
39
40
|
}
|
|
40
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* Read a line from the TTY without echoing characters. Each keypress shows
|
|
44
|
+
* an asterisk so the user has visual feedback for length. Returns the
|
|
45
|
+
* collected string. Ctrl-C aborts the process.
|
|
46
|
+
*
|
|
47
|
+
* Uses raw mode directly because Node's readline always echoes input.
|
|
48
|
+
* Exported so the manual `clementine config keychain-fix-acl` command can
|
|
49
|
+
* use the same one-prompt UX.
|
|
50
|
+
*/
|
|
51
|
+
export function readPasswordFromTty(prompt) {
|
|
52
|
+
return new Promise((resolve) => {
|
|
53
|
+
output.write(prompt);
|
|
54
|
+
let value = '';
|
|
55
|
+
const wasRaw = input.isRaw === true;
|
|
56
|
+
if (typeof input.setRawMode === 'function')
|
|
57
|
+
input.setRawMode(true);
|
|
58
|
+
input.resume();
|
|
59
|
+
input.setEncoding('utf8');
|
|
60
|
+
const finish = () => {
|
|
61
|
+
input.removeListener('data', onData);
|
|
62
|
+
if (typeof input.setRawMode === 'function')
|
|
63
|
+
input.setRawMode(wasRaw);
|
|
64
|
+
input.pause();
|
|
65
|
+
output.write('\n');
|
|
66
|
+
resolve(value);
|
|
67
|
+
};
|
|
68
|
+
const onData = (chunk) => {
|
|
69
|
+
// Raw stdin can deliver multiple chars per chunk (paste, escape seqs).
|
|
70
|
+
for (const char of chunk) {
|
|
71
|
+
const code = char.charCodeAt(0);
|
|
72
|
+
// Ctrl-C → abort the process entirely (user wants out).
|
|
73
|
+
if (code === 0x03) {
|
|
74
|
+
output.write('\n');
|
|
75
|
+
process.exit(130);
|
|
76
|
+
}
|
|
77
|
+
// Enter (LF/CR) or Ctrl-D → submit whatever we have.
|
|
78
|
+
if (code === 0x0a || code === 0x0d || code === 0x04) {
|
|
79
|
+
finish();
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
// Backspace (DEL or BS).
|
|
83
|
+
if (code === 0x7f || code === 0x08) {
|
|
84
|
+
if (value.length > 0) {
|
|
85
|
+
value = value.slice(0, -1);
|
|
86
|
+
output.write('\b \b');
|
|
87
|
+
}
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
// Printable ASCII + UTF-8 — skip anything else (arrow keys, escape seqs).
|
|
91
|
+
if (code >= 0x20 && code !== 0x7f) {
|
|
92
|
+
value += char;
|
|
93
|
+
output.write('*');
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
input.on('data', onData);
|
|
98
|
+
});
|
|
99
|
+
}
|
|
41
100
|
export async function runFirstRunKeychainWizardIfNeeded(baseDir) {
|
|
42
101
|
if (process.platform !== 'darwin')
|
|
43
102
|
return;
|
|
@@ -48,7 +107,6 @@ export async function runFirstRunKeychainWizardIfNeeded(baseDir) {
|
|
|
48
107
|
const { listClementineKeychainEntries, fixAllClementineEntries } = await import('./keychain-fix-acl.js');
|
|
49
108
|
const entries = listClementineKeychainEntries().filter((e) => e.isClementine);
|
|
50
109
|
if (entries.length === 0) {
|
|
51
|
-
// Nothing to fix — write sentinel so we don't re-scan every launch.
|
|
52
110
|
markKeychainWizardDone(baseDir);
|
|
53
111
|
return;
|
|
54
112
|
}
|
|
@@ -62,10 +120,11 @@ export async function runFirstRunKeychainWizardIfNeeded(baseDir) {
|
|
|
62
120
|
console.log(` ${BOLD}One-time keychain setup${RESET}`);
|
|
63
121
|
console.log(` ${DIM}${entries.length} keychain entr${entries.length === 1 ? 'y' : 'ies'} from a previous version need an${RESET}`);
|
|
64
122
|
console.log(` ${DIM}access-control update so Clementine can read them${RESET}`);
|
|
65
|
-
console.log(` ${DIM}silently — otherwise macOS
|
|
123
|
+
console.log(` ${DIM}silently — otherwise macOS prompts on every read.${RESET}`);
|
|
66
124
|
console.log();
|
|
67
|
-
console.log(` ${DIM}
|
|
68
|
-
console.log(` ${DIM}
|
|
125
|
+
console.log(` ${DIM}You'll enter your macOS login password ONCE below —${RESET}`);
|
|
126
|
+
console.log(` ${DIM}it authorizes all ACL updates in a single pass.${RESET}`);
|
|
127
|
+
console.log(` ${DIM}Not stored. Won't ask again.${RESET}`);
|
|
69
128
|
console.log();
|
|
70
129
|
const rl = readline.createInterface({ input, output });
|
|
71
130
|
let answer;
|
|
@@ -77,7 +136,15 @@ export async function runFirstRunKeychainWizardIfNeeded(baseDir) {
|
|
|
77
136
|
}
|
|
78
137
|
if (answer === 'n' || answer === 'no') {
|
|
79
138
|
console.log();
|
|
80
|
-
console.log(` ${YELLOW}Skipped.${RESET} ${DIM}Run later
|
|
139
|
+
console.log(` ${YELLOW}Skipped.${RESET} ${DIM}Run later: clementine config keychain-fix-acl${RESET}`);
|
|
140
|
+
console.log();
|
|
141
|
+
markKeychainWizardDone(baseDir);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
const password = await readPasswordFromTty(` ${BOLD}macOS login password:${RESET} `);
|
|
145
|
+
if (!password) {
|
|
146
|
+
console.log();
|
|
147
|
+
console.log(` ${YELLOW}Empty password — skipped.${RESET} ${DIM}Run later: clementine config keychain-fix-acl${RESET}`);
|
|
81
148
|
console.log();
|
|
82
149
|
markKeychainWizardDone(baseDir);
|
|
83
150
|
return;
|
|
@@ -85,9 +152,10 @@ export async function runFirstRunKeychainWizardIfNeeded(baseDir) {
|
|
|
85
152
|
console.log();
|
|
86
153
|
console.log(` ${BOLD}Repairing ACLs...${RESET}`);
|
|
87
154
|
console.log();
|
|
88
|
-
const results = fixAllClementineEntries();
|
|
155
|
+
const results = fixAllClementineEntries({ keychainPassword: password });
|
|
89
156
|
let okCount = 0;
|
|
90
157
|
let failCount = 0;
|
|
158
|
+
let wrongPasswordHit = false;
|
|
91
159
|
for (const r of results) {
|
|
92
160
|
if (r.status === 'fixed') {
|
|
93
161
|
console.log(` ${GREEN}✓${RESET} ${r.account}`);
|
|
@@ -96,20 +164,31 @@ export async function runFirstRunKeychainWizardIfNeeded(baseDir) {
|
|
|
96
164
|
else if (r.status === 'failed') {
|
|
97
165
|
console.log(` ${RED}✗${RESET} ${r.account} ${DIM}— ${r.error ?? 'unknown'}${RESET}`);
|
|
98
166
|
failCount++;
|
|
167
|
+
// security's stderr for a bad password contains "MAC verification failed"
|
|
168
|
+
// or similar. Catch the common shapes so we can re-prompt next launch.
|
|
169
|
+
if (r.error && /MAC verification|AuthFailure|UserCanceled|-25293/i.test(r.error)) {
|
|
170
|
+
wrongPasswordHit = true;
|
|
171
|
+
}
|
|
99
172
|
}
|
|
100
173
|
}
|
|
101
174
|
console.log();
|
|
102
175
|
if (failCount === 0) {
|
|
103
176
|
console.log(` ${GREEN}Done — ${okCount} entr${okCount === 1 ? 'y' : 'ies'} repaired.${RESET} ${DIM}Future reads silent.${RESET}`);
|
|
104
177
|
}
|
|
178
|
+
else if (wrongPasswordHit && okCount === 0) {
|
|
179
|
+
console.log(` ${RED}Wrong password — no entries repaired.${RESET}`);
|
|
180
|
+
console.log(` ${DIM}We'll ask again on next launch.${RESET}`);
|
|
181
|
+
}
|
|
105
182
|
else {
|
|
106
183
|
console.log(` ${YELLOW}${okCount} fixed, ${failCount} failed.${RESET}`);
|
|
107
184
|
console.log(` ${DIM}Failed entries can be fixed manually in Keychain Access.app:${RESET}`);
|
|
108
185
|
console.log(` ${DIM} search "clementine-agent" → right-click → Get Info → Access Control.${RESET}`);
|
|
109
186
|
}
|
|
110
187
|
console.log();
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
|
|
188
|
+
// Don't write sentinel if the password was wrong AND nothing succeeded —
|
|
189
|
+
// let the user retry on the next launch. Any other outcome marks done.
|
|
190
|
+
if (!(wrongPasswordHit && okCount === 0)) {
|
|
191
|
+
markKeychainWizardDone(baseDir);
|
|
192
|
+
}
|
|
114
193
|
}
|
|
115
194
|
//# sourceMappingURL=keychain-first-run-wizard.js.map
|
|
@@ -51,12 +51,20 @@ export interface AclFixResult {
|
|
|
51
51
|
* never touched (could be other apps that coincidentally chose that name).
|
|
52
52
|
*/
|
|
53
53
|
export declare function listClementineKeychainEntries(): KeychainEntry[];
|
|
54
|
-
export declare function fixAcl(service: Service, account: string
|
|
54
|
+
export declare function fixAcl(service: Service, account: string, opts?: {
|
|
55
|
+
keychainPassword?: string;
|
|
56
|
+
}): AclFixResult;
|
|
55
57
|
/**
|
|
56
58
|
* Plan + apply: enumerate entries, fix each Clementine-shaped one in turn.
|
|
57
59
|
* Foreign entries (other apps under the legacy "clementine" service) get
|
|
58
60
|
* reported with status='skipped-foreign' and never touched.
|
|
61
|
+
*
|
|
62
|
+
* Pass opts.keychainPassword to authorize all entries with one stored password
|
|
63
|
+
* instead of one TTY prompt per entry — that's how the wizard avoids the
|
|
64
|
+
* 7-prompt bombardment.
|
|
59
65
|
*/
|
|
60
|
-
export declare function fixAllClementineEntries(
|
|
66
|
+
export declare function fixAllClementineEntries(opts?: {
|
|
67
|
+
keychainPassword?: string;
|
|
68
|
+
}): AclFixResult[];
|
|
61
69
|
export {};
|
|
62
70
|
//# sourceMappingURL=keychain-fix-acl.d.ts.map
|
|
@@ -148,7 +148,7 @@ function locateKeychain(service, account) {
|
|
|
148
148
|
const m = first.match(/^keychain:\s+"([^"]+)"/);
|
|
149
149
|
return m ? m[1] : null;
|
|
150
150
|
}
|
|
151
|
-
export function fixAcl(service, account) {
|
|
151
|
+
export function fixAcl(service, account, opts = {}) {
|
|
152
152
|
const keychainPath = locateKeychain(service, account);
|
|
153
153
|
if (!keychainPath) {
|
|
154
154
|
return {
|
|
@@ -160,33 +160,47 @@ export function fixAcl(service, account) {
|
|
|
160
160
|
}
|
|
161
161
|
// Pass the keychain path as the trailing positional arg so partition-list
|
|
162
162
|
// doesn't search the wrong store.
|
|
163
|
-
const
|
|
163
|
+
const baseArgs = [
|
|
164
164
|
'set-generic-password-partition-list',
|
|
165
165
|
'-s', service,
|
|
166
166
|
'-a', account,
|
|
167
167
|
'-S', 'apple-tool:,apple:',
|
|
168
|
-
keychainPath,
|
|
169
168
|
];
|
|
169
|
+
// When a password is provided, pass it via -k and capture stdio so security
|
|
170
|
+
// doesn't prompt — that's the whole point. Without -k, security prompts on
|
|
171
|
+
// the inherited TTY for each entry, which means N entries = N prompts.
|
|
172
|
+
const args = opts.keychainPassword
|
|
173
|
+
? [...baseArgs, '-k', opts.keychainPassword, keychainPath]
|
|
174
|
+
: [...baseArgs, keychainPath];
|
|
170
175
|
const result = spawnSync('/usr/bin/security', args, {
|
|
171
|
-
stdio: 'inherit',
|
|
176
|
+
stdio: opts.keychainPassword ? ['ignore', 'pipe', 'pipe'] : 'inherit',
|
|
172
177
|
timeout: 120_000,
|
|
173
178
|
});
|
|
174
179
|
if (result.status === 0) {
|
|
175
180
|
return { service, account, status: 'fixed' };
|
|
176
181
|
}
|
|
182
|
+
// When stdio was captured, surface stderr in the error so callers can
|
|
183
|
+
// detect "wrong password" vs other failures.
|
|
184
|
+
const stderr = opts.keychainPassword && result.stderr
|
|
185
|
+
? result.stderr.toString().trim().slice(0, 200)
|
|
186
|
+
: '';
|
|
177
187
|
return {
|
|
178
188
|
service,
|
|
179
189
|
account,
|
|
180
190
|
status: 'failed',
|
|
181
|
-
error: result.error?.message
|
|
191
|
+
error: stderr || result.error?.message || `exit code ${result.status}`,
|
|
182
192
|
};
|
|
183
193
|
}
|
|
184
194
|
/**
|
|
185
195
|
* Plan + apply: enumerate entries, fix each Clementine-shaped one in turn.
|
|
186
196
|
* Foreign entries (other apps under the legacy "clementine" service) get
|
|
187
197
|
* reported with status='skipped-foreign' and never touched.
|
|
198
|
+
*
|
|
199
|
+
* Pass opts.keychainPassword to authorize all entries with one stored password
|
|
200
|
+
* instead of one TTY prompt per entry — that's how the wizard avoids the
|
|
201
|
+
* 7-prompt bombardment.
|
|
188
202
|
*/
|
|
189
|
-
export function fixAllClementineEntries() {
|
|
203
|
+
export function fixAllClementineEntries(opts = {}) {
|
|
190
204
|
const entries = listClementineKeychainEntries();
|
|
191
205
|
const results = [];
|
|
192
206
|
for (const entry of entries) {
|
|
@@ -194,7 +208,7 @@ export function fixAllClementineEntries() {
|
|
|
194
208
|
results.push({ service: entry.service, account: entry.account, status: 'skipped-foreign' });
|
|
195
209
|
continue;
|
|
196
210
|
}
|
|
197
|
-
results.push(fixAcl(entry.service, entry.account));
|
|
211
|
+
results.push(fixAcl(entry.service, entry.account, opts));
|
|
198
212
|
}
|
|
199
213
|
return results;
|
|
200
214
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clementine TypeScript — Seed user-model slots from existing memory.
|
|
3
|
+
*
|
|
4
|
+
* One-shot Haiku pass over MEMORY.md + top-salience chunks + recent session
|
|
5
|
+
* summaries. Proposes initial values for the 4 user_model slots so a freshly-
|
|
6
|
+
* upgraded agent has a populated mental model immediately rather than needing
|
|
7
|
+
* to re-learn everything through fresh conversations.
|
|
8
|
+
*
|
|
9
|
+
* Returns proposals only — caller decides whether to apply (typically via the
|
|
10
|
+
* dashboard "Seed from existing memory" review UI).
|
|
11
|
+
*/
|
|
12
|
+
interface SeedSourceStore {
|
|
13
|
+
searchFts(query: string, limit: number): Array<{
|
|
14
|
+
chunkId: number;
|
|
15
|
+
sourceFile: string;
|
|
16
|
+
section: string;
|
|
17
|
+
content: string;
|
|
18
|
+
salience: number;
|
|
19
|
+
}>;
|
|
20
|
+
getRecentSummaries(limit?: number): Array<{
|
|
21
|
+
summary: string;
|
|
22
|
+
createdAt: string;
|
|
23
|
+
}>;
|
|
24
|
+
db: unknown;
|
|
25
|
+
}
|
|
26
|
+
export interface UserModelProposals {
|
|
27
|
+
user_facts: string;
|
|
28
|
+
goals: string;
|
|
29
|
+
relationships: string;
|
|
30
|
+
agent_persona: string;
|
|
31
|
+
/** Number of distinct source items the LLM saw (chunks + summaries + MEMORY.md). */
|
|
32
|
+
sourceCount: number;
|
|
33
|
+
/** Raw model output, for debugging. */
|
|
34
|
+
rawResponse?: string;
|
|
35
|
+
}
|
|
36
|
+
export declare function seedUserModelFromMemory(store: SeedSourceStore, llmCall: (prompt: string) => Promise<string>): Promise<UserModelProposals>;
|
|
37
|
+
export {};
|
|
38
|
+
//# sourceMappingURL=seed-user-model.d.ts.map
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clementine TypeScript — Seed user-model slots from existing memory.
|
|
3
|
+
*
|
|
4
|
+
* One-shot Haiku pass over MEMORY.md + top-salience chunks + recent session
|
|
5
|
+
* summaries. Proposes initial values for the 4 user_model slots so a freshly-
|
|
6
|
+
* upgraded agent has a populated mental model immediately rather than needing
|
|
7
|
+
* to re-learn everything through fresh conversations.
|
|
8
|
+
*
|
|
9
|
+
* Returns proposals only — caller decides whether to apply (typically via the
|
|
10
|
+
* dashboard "Seed from existing memory" review UI).
|
|
11
|
+
*/
|
|
12
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
13
|
+
import pino from 'pino';
|
|
14
|
+
import { MEMORY_FILE } from '../tools/shared.js';
|
|
15
|
+
const logger = pino({ name: 'clementine.seed-user-model' });
|
|
16
|
+
/** Size budget per source category — keeps the corpus under Haiku's
|
|
17
|
+
* prompt-cost sweet spot while preserving signal. */
|
|
18
|
+
const MAX_MEMORY_MD_CHARS = 4000;
|
|
19
|
+
const MAX_CHUNK_CHARS = 4000;
|
|
20
|
+
const MAX_SUMMARIES_CHARS = 1500;
|
|
21
|
+
function gatherCorpus(store) {
|
|
22
|
+
const parts = [];
|
|
23
|
+
let sourceCount = 0;
|
|
24
|
+
// 1. MEMORY.md — highest-signal source, the agent's curated profile note
|
|
25
|
+
if (existsSync(MEMORY_FILE)) {
|
|
26
|
+
try {
|
|
27
|
+
const md = readFileSync(MEMORY_FILE, 'utf-8').slice(0, MAX_MEMORY_MD_CHARS);
|
|
28
|
+
if (md.trim()) {
|
|
29
|
+
parts.push(`## MEMORY.md\n${md}`);
|
|
30
|
+
sourceCount++;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch { /* non-fatal */ }
|
|
34
|
+
}
|
|
35
|
+
// 2. Top-salience chunks — what the agent has been actively retrieving
|
|
36
|
+
// We don't have a "list by salience" method on the store directly, but
|
|
37
|
+
// searchFts with a generic query that matches almost everything works as
|
|
38
|
+
// a coarse top-N proxy. Better: query the underlying db handle.
|
|
39
|
+
try {
|
|
40
|
+
const db = store.db;
|
|
41
|
+
const rows = db.prepare(`SELECT c.source_file, c.section, c.content, c.salience
|
|
42
|
+
FROM chunks c
|
|
43
|
+
LEFT JOIN chunk_soft_deletes sd ON sd.chunk_id = c.id
|
|
44
|
+
WHERE sd.chunk_id IS NULL AND length(c.content) > 50
|
|
45
|
+
ORDER BY c.salience DESC, c.last_outcome_score DESC, c.updated_at DESC
|
|
46
|
+
LIMIT 40`).all();
|
|
47
|
+
if (rows.length > 0) {
|
|
48
|
+
let chunkBlock = '## High-salience memory chunks\n';
|
|
49
|
+
let used = 0;
|
|
50
|
+
for (const r of rows) {
|
|
51
|
+
const entry = `\n--- [${r.source_file} · ${r.section}]\n${r.content.slice(0, 500)}\n`;
|
|
52
|
+
if (used + entry.length > MAX_CHUNK_CHARS)
|
|
53
|
+
break;
|
|
54
|
+
chunkBlock += entry;
|
|
55
|
+
used += entry.length;
|
|
56
|
+
sourceCount++;
|
|
57
|
+
}
|
|
58
|
+
parts.push(chunkBlock);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
logger.debug({ err }, 'Failed to gather salience chunks');
|
|
63
|
+
}
|
|
64
|
+
// 3. Recent session summaries — surfaces current goals and active context
|
|
65
|
+
try {
|
|
66
|
+
const summaries = store.getRecentSummaries(8);
|
|
67
|
+
if (summaries.length > 0) {
|
|
68
|
+
let block = '## Recent session summaries\n';
|
|
69
|
+
let used = 0;
|
|
70
|
+
for (const s of summaries) {
|
|
71
|
+
const entry = `\n[${s.createdAt}]\n${s.summary.slice(0, 400)}\n`;
|
|
72
|
+
if (used + entry.length > MAX_SUMMARIES_CHARS)
|
|
73
|
+
break;
|
|
74
|
+
block += entry;
|
|
75
|
+
used += entry.length;
|
|
76
|
+
sourceCount++;
|
|
77
|
+
}
|
|
78
|
+
parts.push(block);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
catch { /* getRecentSummaries may be missing or empty */ }
|
|
82
|
+
return { corpus: parts.join('\n\n---\n\n'), sourceCount };
|
|
83
|
+
}
|
|
84
|
+
const SEED_PROMPT = (corpus) => `Analyze the memory corpus below and propose initial values for the agent's "user model" — coherent always-in-context facts about the user that load into every conversation.
|
|
85
|
+
|
|
86
|
+
The user model has 4 slots, each capped at 2000 chars:
|
|
87
|
+
|
|
88
|
+
1. **user_facts** — Who they are. Name, role, location, lasting preferences (writing style, tools, communication style). Stable identifiers and personality traits.
|
|
89
|
+
2. **goals** — What they're actively working toward right now. Current projects, deadlines, immediate intents. Skip vague long-term aspirations.
|
|
90
|
+
3. **relationships** — People, projects, channels they regularly interact with. One line each, who-is-what.
|
|
91
|
+
4. **agent_persona** — How the agent (Clementine) should think of itself in relation to the user. Skip if no signal.
|
|
92
|
+
|
|
93
|
+
Rules:
|
|
94
|
+
- Only include facts with direct evidence in the corpus
|
|
95
|
+
- Be terse. Bullet fragments, not full sentences. Skip filler.
|
|
96
|
+
- If a slot has no good signal, output exactly: "(no clear signal)"
|
|
97
|
+
- Never invent details — if uncertain, leave it out
|
|
98
|
+
- Don't repeat the same fact across slots
|
|
99
|
+
|
|
100
|
+
Output exactly this format (markdown headings, no other text before or after):
|
|
101
|
+
|
|
102
|
+
## user_facts
|
|
103
|
+
- bullet
|
|
104
|
+
- bullet
|
|
105
|
+
|
|
106
|
+
## goals
|
|
107
|
+
- bullet
|
|
108
|
+
|
|
109
|
+
## relationships
|
|
110
|
+
- bullet
|
|
111
|
+
|
|
112
|
+
## agent_persona
|
|
113
|
+
- bullet
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
Memory corpus to analyze:
|
|
118
|
+
|
|
119
|
+
${corpus}`;
|
|
120
|
+
function parseProposals(raw) {
|
|
121
|
+
const slots = ['user_facts', 'goals', 'relationships', 'agent_persona'];
|
|
122
|
+
const out = {
|
|
123
|
+
user_facts: '', goals: '', relationships: '', agent_persona: '',
|
|
124
|
+
};
|
|
125
|
+
// Tolerant parser: split on any "## slot_name" header (case-insensitive),
|
|
126
|
+
// anchored at line start. Whatever follows up to the next slot header is
|
|
127
|
+
// that slot's content.
|
|
128
|
+
for (let i = 0; i < slots.length; i++) {
|
|
129
|
+
const slot = slots[i];
|
|
130
|
+
const next = slots[i + 1];
|
|
131
|
+
const startRe = new RegExp(`^\\s*##\\s+${slot}\\b.*$`, 'mi');
|
|
132
|
+
const m = startRe.exec(raw);
|
|
133
|
+
if (!m)
|
|
134
|
+
continue;
|
|
135
|
+
let endIdx = raw.length;
|
|
136
|
+
if (next) {
|
|
137
|
+
const endRe = new RegExp(`^\\s*##\\s+${next}\\b.*$`, 'mi');
|
|
138
|
+
const e = endRe.exec(raw);
|
|
139
|
+
if (e && e.index > m.index)
|
|
140
|
+
endIdx = e.index;
|
|
141
|
+
}
|
|
142
|
+
let body = raw.slice(m.index + m[0].length, endIdx).trim();
|
|
143
|
+
// Treat "(no clear signal)" as empty so the slot stays unset
|
|
144
|
+
if (/^\s*\(?no\s+clear\s+signal\)?\s*$/i.test(body))
|
|
145
|
+
body = '';
|
|
146
|
+
out[slot] = body.slice(0, 2000);
|
|
147
|
+
}
|
|
148
|
+
return out;
|
|
149
|
+
}
|
|
150
|
+
export async function seedUserModelFromMemory(store, llmCall) {
|
|
151
|
+
const { corpus, sourceCount } = gatherCorpus(store);
|
|
152
|
+
if (!corpus.trim() || sourceCount === 0) {
|
|
153
|
+
return {
|
|
154
|
+
user_facts: '', goals: '', relationships: '', agent_persona: '',
|
|
155
|
+
sourceCount: 0,
|
|
156
|
+
rawResponse: 'No source material found — vault may be empty.',
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
const prompt = SEED_PROMPT(corpus);
|
|
160
|
+
let raw = '';
|
|
161
|
+
try {
|
|
162
|
+
raw = await llmCall(prompt);
|
|
163
|
+
}
|
|
164
|
+
catch (err) {
|
|
165
|
+
logger.warn({ err }, 'Seed LLM call failed');
|
|
166
|
+
return {
|
|
167
|
+
user_facts: '', goals: '', relationships: '', agent_persona: '',
|
|
168
|
+
sourceCount, rawResponse: `LLM call failed: ${String(err)}`,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
const parsed = parseProposals(raw);
|
|
172
|
+
return { ...parsed, sourceCount, rawResponse: raw };
|
|
173
|
+
}
|
|
174
|
+
//# sourceMappingURL=seed-user-model.js.map
|