clementine-agent 1.0.1 → 1.0.2

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.
@@ -26,6 +26,10 @@ export interface ProjectMeta {
26
26
  export declare function findProjectByName(query: string): ProjectMeta | null;
27
27
  /** Get all linked projects. */
28
28
  export declare function getLinkedProjects(): ProjectMeta[];
29
+ /** Add a project to the linked projects list. */
30
+ export declare function addProject(projectPath: string, description?: string, keywords?: string[]): void;
31
+ /** Remove a project from the linked projects list. Returns true if removed. */
32
+ export declare function removeProject(projectPath: string): boolean;
29
33
  export declare class PersonalAssistant {
30
34
  static readonly MAX_SESSION_EXCHANGES = 40;
31
35
  private sessions;
@@ -50,6 +54,8 @@ export declare class PersonalAssistant {
50
54
  /** Per-session stall nudge — set after a query shows stall signals, consumed on the next query. */
51
55
  private stallNudges;
52
56
  private _compactedSessions;
57
+ /** Last auto-matched project per session — exposed for CLI display. */
58
+ private _lastMatchedProject;
53
59
  /** Hot correction buffer — explicit behavioral corrections applied before nightly SI. */
54
60
  private hotCorrections;
55
61
  constructor();
@@ -113,6 +119,11 @@ export declare class PersonalAssistant {
113
119
  * No LLM call — uses buildLocalSummary for instant summarization.
114
120
  */
115
121
  private compactContext;
122
+ /**
123
+ * Expire sessions inactive for more than 24 hours.
124
+ * Called periodically from chat() to prevent unbounded map growth.
125
+ */
126
+ private expireOldSessions;
116
127
  /**
117
128
  * Build an instant local summary from in-memory exchange history.
118
129
  * No LLM call — returns immediately. Used during session rotation
@@ -220,6 +231,8 @@ export declare class PersonalAssistant {
220
231
  content: string;
221
232
  }>>;
222
233
  clearSession(sessionKey: string): void;
234
+ /** Get the last auto-matched project for a session (for CLI display). */
235
+ getLastMatchedProject(sessionKey: string): ProjectMeta | null;
223
236
  getProfileManager(): AgentManager;
224
237
  }
225
238
  //# sourceMappingURL=assistant.d.ts.map
@@ -469,6 +469,33 @@ export function findProjectByName(query) {
469
469
  export function getLinkedProjects() {
470
470
  return loadProjectsMeta();
471
471
  }
472
+ /** Add a project to the linked projects list. */
473
+ export function addProject(projectPath, description, keywords) {
474
+ const resolved = path.resolve(projectPath);
475
+ const projects = loadProjectsMeta();
476
+ // Avoid duplicates
477
+ if (projects.some(p => path.resolve(p.path) === resolved))
478
+ return;
479
+ const entry = { path: resolved };
480
+ if (description)
481
+ entry.description = description;
482
+ if (keywords?.length)
483
+ entry.keywords = keywords;
484
+ projects.push(entry);
485
+ fs.writeFileSync(PROJECTS_META_FILE, JSON.stringify(projects, null, 4));
486
+ _projectsMetaCacheTime = 0; // invalidate cache
487
+ }
488
+ /** Remove a project from the linked projects list. Returns true if removed. */
489
+ export function removeProject(projectPath) {
490
+ const resolved = path.resolve(projectPath);
491
+ const projects = loadProjectsMeta();
492
+ const filtered = projects.filter(p => path.resolve(p.path) !== resolved);
493
+ if (filtered.length === projects.length)
494
+ return false;
495
+ fs.writeFileSync(PROJECTS_META_FILE, JSON.stringify(filtered, null, 4));
496
+ _projectsMetaCacheTime = 0; // invalidate cache
497
+ return true;
498
+ }
472
499
  // ── PersonalAssistant ───────────────────────────────────────────────
473
500
  export class PersonalAssistant {
474
501
  static MAX_SESSION_EXCHANGES = MAX_SESSION_EXCHANGES;
@@ -494,6 +521,8 @@ export class PersonalAssistant {
494
521
  /** Per-session stall nudge — set after a query shows stall signals, consumed on the next query. */
495
522
  stallNudges = new Map();
496
523
  _compactedSessions = new Set();
524
+ /** Last auto-matched project per session — exposed for CLI display. */
525
+ _lastMatchedProject = new Map();
497
526
  /** Hot correction buffer — explicit behavioral corrections applied before nightly SI. */
498
527
  hotCorrections = [];
499
528
  constructor() {
@@ -1482,6 +1511,8 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
1482
1511
  const key = sessionKey ?? undefined;
1483
1512
  this._lastUserMessage = text;
1484
1513
  let sessionRotated = false;
1514
+ // Periodic cleanup: expire all sessions older than 24 hours
1515
+ this.expireOldSessions();
1485
1516
  // Expire old sessions (4 hours)
1486
1517
  if (key && this.sessionTimestamps.has(key)) {
1487
1518
  const elapsed = Date.now() - this.sessionTimestamps.get(key).getTime();
@@ -1735,6 +1766,9 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
1735
1766
  setSendPolicy(profile?.sendPolicy ?? null, profile?.slug ?? null);
1736
1767
  setAgentDir(profile?.agentDir ?? null);
1737
1768
  setInteractionSource(inferInteractionSource(sessionKey));
1769
+ // Track the matched project for CLI display
1770
+ if (sessionKey)
1771
+ this._lastMatchedProject.set(sessionKey, matchedProject);
1738
1772
  if (matchedProject) {
1739
1773
  logger.info({ project: matchedProject.path }, 'Auto-matched project from message');
1740
1774
  const projName = path.basename(matchedProject.path);
@@ -2133,13 +2167,17 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
2133
2167
  return;
2134
2168
  // Build compaction block for working memory
2135
2169
  const exchangeCount = this.exchangeCounts.get(sessionKey) ?? 0;
2170
+ const COMPACTION_START = '<!-- COMPACTION_START -->';
2171
+ const COMPACTION_END = '<!-- COMPACTION_END -->';
2136
2172
  const compactionBlock = [
2173
+ COMPACTION_START,
2137
2174
  `## Session Compaction (auto-generated)`,
2138
2175
  `Session ${sessionKey} compacted at ${exchangeCount} exchanges.`,
2139
2176
  ``,
2140
2177
  summary,
2141
2178
  ``,
2142
2179
  `*Continue from where this conversation left off.*`,
2180
+ COMPACTION_END,
2143
2181
  ].join('\n');
2144
2182
  // Write to working memory so the next session picks it up via system prompt
2145
2183
  try {
@@ -2150,11 +2188,23 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
2150
2188
  const existing = fs.existsSync(compactionWmFile)
2151
2189
  ? fs.readFileSync(compactionWmFile, 'utf-8')
2152
2190
  : '';
2153
- // Replace any prior compaction block, or append
2154
- const compactionRegex = /## Session Compaction \(auto-generated\)[\s\S]*?\*Continue from where this conversation left off\.\*/;
2155
- const updated = compactionRegex.test(existing)
2156
- ? existing.replace(compactionRegex, compactionBlock)
2157
- : existing.trimEnd() + '\n\n' + compactionBlock;
2191
+ // Replace any prior compaction block (try new sentinel format first, then legacy)
2192
+ const sentinelRegex = /<!-- COMPACTION_START -->[\s\S]*?<!-- COMPACTION_END -->/;
2193
+ const legacyRegex = /## Session Compaction \(auto-generated\)[\s\S]*?\*Continue from where this conversation left off\.\*/;
2194
+ let updated;
2195
+ if (sentinelRegex.test(existing)) {
2196
+ updated = existing.replace(sentinelRegex, compactionBlock);
2197
+ }
2198
+ else if (legacyRegex.test(existing)) {
2199
+ updated = existing.replace(legacyRegex, compactionBlock);
2200
+ }
2201
+ else {
2202
+ updated = existing.trimEnd() + '\n\n' + compactionBlock;
2203
+ }
2204
+ // Size guard: if working memory exceeds 10KB, keep only the compaction block
2205
+ if (Buffer.byteLength(updated) > 10_240) {
2206
+ updated = compactionBlock;
2207
+ }
2158
2208
  fs.writeFileSync(compactionWmFile, updated);
2159
2209
  }
2160
2210
  catch {
@@ -2164,8 +2214,24 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
2164
2214
  // The working memory summary will provide continuity
2165
2215
  this.sessions.delete(sessionKey);
2166
2216
  this.exchangeCounts.set(sessionKey, 0);
2217
+ this.lastExchanges.delete(sessionKey);
2218
+ this.sessionTimestamps.delete(sessionKey);
2219
+ this.stallNudges.delete(sessionKey);
2167
2220
  this.saveSessions();
2168
2221
  }
2222
+ /**
2223
+ * Expire sessions inactive for more than 24 hours.
2224
+ * Called periodically from chat() to prevent unbounded map growth.
2225
+ */
2226
+ expireOldSessions() {
2227
+ const MAX_AGE_MS = 24 * 60 * 60 * 1000;
2228
+ const now = Date.now();
2229
+ for (const [key, ts] of this.sessionTimestamps) {
2230
+ if (now - ts.getTime() > MAX_AGE_MS) {
2231
+ this.clearSession(key);
2232
+ }
2233
+ }
2234
+ }
2169
2235
  // ── Session Summarization ─────────────────────────────────────────
2170
2236
  /**
2171
2237
  * Build an instant local summary from in-memory exchange history.
@@ -3879,8 +3945,13 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
3879
3945
  this.sessionTimestamps.delete(sessionKey);
3880
3946
  this.lastExchanges.delete(sessionKey);
3881
3947
  this.stallNudges.delete(sessionKey);
3948
+ this._lastMatchedProject.delete(sessionKey);
3882
3949
  this.saveSessions();
3883
3950
  }
3951
+ /** Get the last auto-matched project for a session (for CLI display). */
3952
+ getLastMatchedProject(sessionKey) {
3953
+ return this._lastMatchedProject.get(sessionKey) ?? null;
3954
+ }
3884
3955
  getProfileManager() {
3885
3956
  return this.profileManager;
3886
3957
  }
package/dist/cli/chat.js CHANGED
@@ -202,6 +202,11 @@ export async function cmdChat(opts) {
202
202
  const response = await gateway.handleMessage(sessionKey, effectiveText, undefined, oneOffModel);
203
203
  // Clear "thinking..." line
204
204
  process.stdout.write('\x1b[2K\r');
205
+ // Show active project if auto-matched
206
+ const matched = gateway.getLastMatchedProject(sessionKey);
207
+ if (matched) {
208
+ console.log(`\x1b[0;90m[project: ${path.basename(matched.path)}]\x1b[0m`);
209
+ }
205
210
  console.log(renderMarkdown(response));
206
211
  console.log();
207
212
  }
package/dist/cli/index.js CHANGED
@@ -1295,6 +1295,131 @@ configCmd
1295
1295
  .command('list')
1296
1296
  .description('List all config values')
1297
1297
  .action(cmdConfigList);
1298
+ configCmd
1299
+ .command('edit')
1300
+ .description('Open .env in your editor')
1301
+ .action(() => {
1302
+ if (!existsSync(ENV_PATH)) {
1303
+ console.log(' No .env file found. Run: clementine config setup');
1304
+ process.exit(1);
1305
+ }
1306
+ const editor = process.env.EDITOR || process.env.VISUAL || 'vi';
1307
+ try {
1308
+ execSync(`${editor} "${ENV_PATH}"`, { stdio: 'inherit' });
1309
+ }
1310
+ catch {
1311
+ console.error(` Failed to open editor: ${editor}`);
1312
+ }
1313
+ });
1314
+ // ── Memory commands ─────────────────────────────────────────────────
1315
+ const memoryCmd = program
1316
+ .command('memory')
1317
+ .description('Search and manage memory');
1318
+ memoryCmd
1319
+ .command('search <query>')
1320
+ .description('Search memory (full-text)')
1321
+ .option('-n, --limit <n>', 'Max results', '10')
1322
+ .action(async (query, opts) => {
1323
+ const DIM = '\x1b[0;90m';
1324
+ const BOLD = '\x1b[1m';
1325
+ const CYAN = '\x1b[0;36m';
1326
+ const RESET = '\x1b[0m';
1327
+ try {
1328
+ const { MemoryStore } = await import('../memory/store.js');
1329
+ const VAULT_DIR = path.join(BASE_DIR, 'vault');
1330
+ const DB_PATH = path.join(VAULT_DIR, '.memory.db');
1331
+ const store = new MemoryStore(DB_PATH, VAULT_DIR);
1332
+ const results = store.searchFts(query, parseInt(opts.limit, 10));
1333
+ if (results.length === 0) {
1334
+ console.log(` No results for "${query}".`);
1335
+ return;
1336
+ }
1337
+ console.log();
1338
+ for (const r of results) {
1339
+ const source = r.sourceFile ? path.basename(r.sourceFile) : 'unknown';
1340
+ const section = r.section || '';
1341
+ const snippet = r.content.replace(/\n/g, ' ').slice(0, 120);
1342
+ console.log(` ${BOLD}${source}${RESET}${section ? ` › ${CYAN}${section}${RESET}` : ''}`);
1343
+ console.log(` ${DIM}${snippet}${snippet.length >= 120 ? '…' : ''}${RESET}`);
1344
+ console.log();
1345
+ }
1346
+ }
1347
+ catch (err) {
1348
+ console.error(` Error searching memory: ${err}`);
1349
+ }
1350
+ });
1351
+ // ── Projects commands ───────────────────────────────────────────────
1352
+ const projectsCmd = program
1353
+ .command('projects')
1354
+ .description('Manage linked projects');
1355
+ projectsCmd
1356
+ .command('list')
1357
+ .description('Show all linked projects')
1358
+ .action(async () => {
1359
+ const DIM = '\x1b[0;90m';
1360
+ const BOLD = '\x1b[1m';
1361
+ const RESET = '\x1b[0m';
1362
+ try {
1363
+ const { getLinkedProjects } = await import('../agent/assistant.js');
1364
+ const projects = getLinkedProjects();
1365
+ if (projects.length === 0) {
1366
+ console.log(' No projects linked. Use: clementine projects add <path>');
1367
+ return;
1368
+ }
1369
+ console.log();
1370
+ for (const p of projects) {
1371
+ console.log(` ${BOLD}${path.basename(p.path)}${RESET}`);
1372
+ console.log(` ${DIM}${p.path}${RESET}`);
1373
+ if (p.description)
1374
+ console.log(` ${p.description}`);
1375
+ if (p.keywords?.length)
1376
+ console.log(` ${DIM}Keywords: ${p.keywords.join(', ')}${RESET}`);
1377
+ console.log();
1378
+ }
1379
+ }
1380
+ catch (err) {
1381
+ console.error(` Error listing projects: ${err}`);
1382
+ }
1383
+ });
1384
+ projectsCmd
1385
+ .command('add <path>')
1386
+ .description('Link a project directory')
1387
+ .option('-d, --description <desc>', 'Project description')
1388
+ .option('-k, --keywords <kw>', 'Comma-separated keywords')
1389
+ .action(async (projectPath, opts) => {
1390
+ const resolved = path.resolve(projectPath);
1391
+ if (!existsSync(resolved) || !statSync(resolved).isDirectory()) {
1392
+ console.error(` Not a directory: ${resolved}`);
1393
+ process.exit(1);
1394
+ }
1395
+ try {
1396
+ const { addProject } = await import('../agent/assistant.js');
1397
+ const keywords = opts.keywords?.split(',').map(k => k.trim()).filter(Boolean);
1398
+ addProject(resolved, opts.description, keywords);
1399
+ console.log(` Linked: ${resolved}`);
1400
+ }
1401
+ catch (err) {
1402
+ console.error(` Error adding project: ${err}`);
1403
+ }
1404
+ });
1405
+ projectsCmd
1406
+ .command('remove <path>')
1407
+ .description('Unlink a project directory')
1408
+ .action(async (projectPath) => {
1409
+ const resolved = path.resolve(projectPath);
1410
+ try {
1411
+ const { removeProject } = await import('../agent/assistant.js');
1412
+ if (removeProject(resolved)) {
1413
+ console.log(` Removed: ${resolved}`);
1414
+ }
1415
+ else {
1416
+ console.log(` Not found: ${resolved}`);
1417
+ }
1418
+ }
1419
+ catch (err) {
1420
+ console.error(` Error removing project: ${err}`);
1421
+ }
1422
+ });
1298
1423
  // ── Update command ──────────────────────────────────────────────────
1299
1424
  async function cmdUpdate(options) {
1300
1425
  const DIM = '\x1b[0;90m';
@@ -196,6 +196,11 @@ export declare class Gateway {
196
196
  memoryCount: number;
197
197
  };
198
198
  clearSession(sessionKey: string): void;
199
+ /** Get the last auto-matched project for a session. */
200
+ getLastMatchedProject(sessionKey: string): {
201
+ path: string;
202
+ description?: string;
203
+ } | null;
199
204
  /** Evict stale session entries (no activity in 48h, no active lock). */
200
205
  evictStaleSessions(): number;
201
206
  /** Get all active session provenance entries (for dashboard/monitoring). */
@@ -1195,6 +1195,10 @@ export class Gateway {
1195
1195
  this.assistant.clearSession(sessionKey);
1196
1196
  this.sessions.delete(sessionKey);
1197
1197
  }
1198
+ /** Get the last auto-matched project for a session. */
1199
+ getLastMatchedProject(sessionKey) {
1200
+ return this.assistant.getLastMatchedProject(sessionKey);
1201
+ }
1198
1202
  /** Evict stale session entries (no activity in 48h, no active lock). */
1199
1203
  evictStaleSessions() {
1200
1204
  const cutoff = Date.now() - 48 * 60 * 60 * 1000;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clementine-agent",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "Clementine — Personal AI Assistant (TypeScript)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",