micode 0.3.1 → 0.3.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.
package/dist/index.js CHANGED
@@ -869,6 +869,8 @@ Create handoff document to transfer context to future session.
869
869
  </when-to-use>
870
870
 
871
871
  <rules>
872
+ <rule>FIRST check for existing ledger at thoughts/ledgers/CONTINUITY_*.md</rule>
873
+ <rule>If ledger exists, use its session name for handoff directory</rule>
872
874
  <rule>Capture ALL in-progress work</rule>
873
875
  <rule>Include exact file:line references for changes</rule>
874
876
  <rule>Document learnings and gotchas</rule>
@@ -878,6 +880,8 @@ Create handoff document to transfer context to future session.
878
880
  </rules>
879
881
 
880
882
  <process>
883
+ <step>Check for ledger at thoughts/ledgers/CONTINUITY_*.md</step>
884
+ <step>If ledger exists, extract session name and state</step>
881
885
  <step>Review what was worked on</step>
882
886
  <step>Check git status for uncommitted changes</step>
883
887
  <step>Gather learnings and decisions made</step>
@@ -886,13 +890,17 @@ Create handoff document to transfer context to future session.
886
890
  <step>Commit handoff document</step>
887
891
  </process>
888
892
 
889
- <output-path>thoughts/shared/handoffs/YYYY-MM-DD_HH-MM-SS_description.md</output-path>
893
+ <output-path>
894
+ If ledger exists: thoughts/shared/handoffs/{session-name}/YYYY-MM-DD_HH-MM-SS.md
895
+ Otherwise: thoughts/shared/handoffs/YYYY-MM-DD_HH-MM-SS_description.md
896
+ </output-path>
890
897
 
891
898
  <document-format>
892
899
  <frontmatter>
893
900
  date: [ISO datetime]
894
901
  branch: [branch name]
895
902
  commit: [hash]
903
+ session: [session name from ledger, if available]
896
904
  </frontmatter>
897
905
  <sections>
898
906
  <section name="Tasks">Table with Task | Status (completed/in-progress/blocked)</section>
@@ -1316,6 +1324,116 @@ var projectInitializerAgent = {
1316
1324
  prompt: PROMPT2
1317
1325
  };
1318
1326
 
1327
+ // src/agents/ledger-creator.ts
1328
+ var ledgerCreatorAgent = {
1329
+ description: "Creates and updates continuity ledgers for session state preservation",
1330
+ mode: "subagent",
1331
+ model: "anthropic/claude-sonnet-4-20250514",
1332
+ temperature: 0.2,
1333
+ tools: {
1334
+ edit: false,
1335
+ task: false
1336
+ },
1337
+ prompt: `<purpose>
1338
+ Create or update a continuity ledger to preserve session state across context clears.
1339
+ The ledger captures the essential context needed to resume work seamlessly.
1340
+ </purpose>
1341
+
1342
+ <rules>
1343
+ <rule>Keep the ledger CONCISE - only essential information</rule>
1344
+ <rule>Focus on WHAT and WHY, not HOW</rule>
1345
+ <rule>State should have exactly ONE item in "Now"</rule>
1346
+ <rule>Mark uncertain information as UNCONFIRMED</rule>
1347
+ <rule>Include git branch and key file paths</rule>
1348
+ </rules>
1349
+
1350
+ <process>
1351
+ <step>Check for existing ledger at thoughts/ledgers/CONTINUITY_*.md</step>
1352
+ <step>If exists, read and update it</step>
1353
+ <step>If not, create new ledger with session name from current task</step>
1354
+ <step>Gather current state: goal, decisions, progress, blockers</step>
1355
+ <step>Write ledger in the exact format below</step>
1356
+ </process>
1357
+
1358
+ <output-path>thoughts/ledgers/CONTINUITY_{session-name}.md</output-path>
1359
+
1360
+ <ledger-format>
1361
+ # Session: {session-name}
1362
+ Updated: {ISO timestamp}
1363
+
1364
+ ## Goal
1365
+ {One sentence describing success criteria}
1366
+
1367
+ ## Constraints
1368
+ {Technical requirements, patterns to follow, things to avoid}
1369
+
1370
+ ## Key Decisions
1371
+ - {Decision}: {Rationale}
1372
+
1373
+ ## State
1374
+ - Done: {Completed items as comma-separated list}
1375
+ - Now: {Current focus - exactly ONE thing}
1376
+ - Next: {Queued items in priority order}
1377
+
1378
+ ## Open Questions
1379
+ - UNCONFIRMED: {Things needing verification}
1380
+
1381
+ ## Working Set
1382
+ - Branch: \`{branch-name}\`
1383
+ - Key files: \`{paths}\`
1384
+ </ledger-format>
1385
+
1386
+ <output-summary>
1387
+ Ledger updated: thoughts/ledgers/CONTINUITY_{session-name}.md
1388
+ State: {Now item}
1389
+ </output-summary>`
1390
+ };
1391
+
1392
+ // src/agents/artifact-searcher.ts
1393
+ var artifactSearcherAgent = {
1394
+ description: "Searches past handoffs, plans, and ledgers for relevant precedent",
1395
+ mode: "subagent",
1396
+ model: "anthropic/claude-sonnet-4-20250514",
1397
+ temperature: 0.3,
1398
+ tools: {
1399
+ edit: false,
1400
+ task: false
1401
+ },
1402
+ prompt: `<purpose>
1403
+ Search the artifact index to find relevant past work, patterns, and lessons learned.
1404
+ Help the user discover precedent from previous sessions.
1405
+ </purpose>
1406
+
1407
+ <rules>
1408
+ <rule>Use artifact_search tool to query the index</rule>
1409
+ <rule>Explain WHY each result is relevant to the query</rule>
1410
+ <rule>Suggest which files to read for more detail</rule>
1411
+ <rule>If no results, suggest alternative search terms</rule>
1412
+ <rule>Highlight learnings and patterns that might apply</rule>
1413
+ </rules>
1414
+
1415
+ <process>
1416
+ <step>Understand what the user is looking for</step>
1417
+ <step>Formulate effective search query</step>
1418
+ <step>Execute search with artifact_search tool</step>
1419
+ <step>Analyze and explain results</step>
1420
+ <step>Recommend next steps (files to read, patterns to apply)</step>
1421
+ </process>
1422
+
1423
+ <output-format>
1424
+ ## Search: {query}
1425
+
1426
+ ### Relevant Results
1427
+ {For each result: explain relevance and key takeaways}
1428
+
1429
+ ### Recommendations
1430
+ {Which files to read, patterns to consider}
1431
+
1432
+ ### Alternative Searches
1433
+ {If results sparse, suggest other queries}
1434
+ </output-format>`
1435
+ };
1436
+
1319
1437
  // src/agents/index.ts
1320
1438
  var agents = {
1321
1439
  [PRIMARY_AGENT_NAME]: primaryAgent,
@@ -1329,7 +1447,9 @@ var agents = {
1329
1447
  executor: executorAgent,
1330
1448
  "handoff-creator": handoffCreatorAgent,
1331
1449
  "handoff-resumer": handoffResumerAgent,
1332
- "project-initializer": projectInitializerAgent
1450
+ "project-initializer": projectInitializerAgent,
1451
+ "ledger-creator": ledgerCreatorAgent,
1452
+ "artifact-searcher": artifactSearcherAgent
1333
1453
  };
1334
1454
 
1335
1455
  // src/tools/ast-grep/index.ts
@@ -13744,7 +13864,7 @@ function formatMatches(matches, isDryRun = false) {
13744
13864
  const shown = matches.slice(0, MAX);
13745
13865
  const lines = shown.map((m) => {
13746
13866
  const loc = `${m.file}:${m.range.start.line}:${m.range.start.column}`;
13747
- const text = m.text.length > 100 ? m.text.slice(0, 100) + "..." : m.text;
13867
+ const text = m.text.length > 100 ? `${m.text.slice(0, 100)}...` : m.text;
13748
13868
  if (isDryRun && m.replacement) {
13749
13869
  return `${loc}
13750
13870
  - ${text}
@@ -13788,16 +13908,7 @@ var ast_grep_replace = tool({
13788
13908
  apply: tool.schema.boolean().optional().describe("Apply changes (default: false, dry-run)")
13789
13909
  },
13790
13910
  execute: async (args) => {
13791
- const sgArgs = [
13792
- "run",
13793
- "-p",
13794
- args.pattern,
13795
- "-r",
13796
- args.rewrite,
13797
- "--lang",
13798
- args.lang,
13799
- "--json=compact"
13800
- ];
13911
+ const sgArgs = ["run", "-p", args.pattern, "-r", args.rewrite, "--lang", args.lang, "--json=compact"];
13801
13912
  if (args.apply) {
13802
13913
  sgArgs.push("--update-all");
13803
13914
  }
@@ -13812,7 +13923,7 @@ var ast_grep_replace = tool({
13812
13923
  const isDryRun = !args.apply;
13813
13924
  const output = formatMatches(result.matches, isDryRun);
13814
13925
  if (isDryRun && result.matches.length > 0) {
13815
- return output + `
13926
+ return `${output}
13816
13927
 
13817
13928
  (Dry run - use apply=true to apply changes)`;
13818
13929
  }
@@ -13829,7 +13940,20 @@ import { readFileSync, statSync } from "fs";
13829
13940
  import { extname, basename } from "path";
13830
13941
  var LARGE_FILE_THRESHOLD = 100 * 1024;
13831
13942
  var MAX_LINES_WITHOUT_EXTRACT = 200;
13832
- var EXTRACTABLE_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".rs", ".java", ".md", ".json", ".yaml", ".yml"];
13943
+ var EXTRACTABLE_EXTENSIONS = [
13944
+ ".ts",
13945
+ ".tsx",
13946
+ ".js",
13947
+ ".jsx",
13948
+ ".py",
13949
+ ".go",
13950
+ ".rs",
13951
+ ".java",
13952
+ ".md",
13953
+ ".json",
13954
+ ".yaml",
13955
+ ".yml"
13956
+ ];
13833
13957
  function extractStructure(content, ext) {
13834
13958
  const lines = content.split(`
13835
13959
  `);
@@ -13861,7 +13985,7 @@ function extractTypeScriptStructure(lines) {
13861
13985
  const line = lines[i];
13862
13986
  const trimmed = line.trim();
13863
13987
  if (trimmed.startsWith("export ") || trimmed.startsWith("class ") || trimmed.startsWith("interface ") || trimmed.startsWith("type ") || trimmed.startsWith("function ") || trimmed.startsWith("const ") || trimmed.startsWith("async function ")) {
13864
- const signature = trimmed.length > 80 ? trimmed.slice(0, 80) + "..." : trimmed;
13988
+ const signature = trimmed.length > 80 ? `${trimmed.slice(0, 80)}...` : trimmed;
13865
13989
  output.push(`Line ${i + 1}: ${signature}`);
13866
13990
  }
13867
13991
  }
@@ -13875,7 +13999,7 @@ function extractPythonStructure(lines) {
13875
13999
  const line = lines[i];
13876
14000
  const trimmed = line.trim();
13877
14001
  if (trimmed.startsWith("class ") || trimmed.startsWith("def ") || trimmed.startsWith("async def ") || trimmed.startsWith("@")) {
13878
- const signature = trimmed.length > 80 ? trimmed.slice(0, 80) + "..." : trimmed;
14002
+ const signature = trimmed.length > 80 ? `${trimmed.slice(0, 80)}...` : trimmed;
13879
14003
  output.push(`Line ${i + 1}: ${signature}`);
13880
14004
  }
13881
14005
  }
@@ -13889,7 +14013,7 @@ function extractGoStructure(lines) {
13889
14013
  const line = lines[i];
13890
14014
  const trimmed = line.trim();
13891
14015
  if (trimmed.startsWith("type ") || trimmed.startsWith("func ") || trimmed.startsWith("package ")) {
13892
- const signature = trimmed.length > 80 ? trimmed.slice(0, 80) + "..." : trimmed;
14016
+ const signature = trimmed.length > 80 ? `${trimmed.slice(0, 80)}...` : trimmed;
13893
14017
  output.push(`Line ${i + 1}: ${signature}`);
13894
14018
  }
13895
14019
  }
@@ -13988,6 +14112,302 @@ ${content}`;
13988
14112
  }
13989
14113
  });
13990
14114
 
14115
+ // src/tools/artifact-index/index.ts
14116
+ import { Database } from "bun:sqlite";
14117
+ import { readFileSync as readFileSync2 } from "fs";
14118
+ import { join, dirname } from "path";
14119
+ import { mkdirSync, existsSync } from "fs";
14120
+ import { homedir } from "os";
14121
+ var DEFAULT_DB_DIR = join(homedir(), ".config", "opencode", "artifact-index");
14122
+ var DB_NAME = "context.db";
14123
+
14124
+ class ArtifactIndex {
14125
+ db = null;
14126
+ dbPath;
14127
+ constructor(dbDir = DEFAULT_DB_DIR) {
14128
+ this.dbPath = join(dbDir, DB_NAME);
14129
+ }
14130
+ async initialize() {
14131
+ const dir = dirname(this.dbPath);
14132
+ if (!existsSync(dir)) {
14133
+ mkdirSync(dir, { recursive: true });
14134
+ }
14135
+ this.db = new Database(this.dbPath);
14136
+ const schemaPath = join(dirname(import.meta.path), "schema.sql");
14137
+ let schema;
14138
+ try {
14139
+ schema = readFileSync2(schemaPath, "utf-8");
14140
+ } catch {
14141
+ schema = this.getInlineSchema();
14142
+ }
14143
+ this.db.exec(schema);
14144
+ }
14145
+ getInlineSchema() {
14146
+ return `
14147
+ CREATE TABLE IF NOT EXISTS handoffs (
14148
+ id TEXT PRIMARY KEY,
14149
+ session_name TEXT,
14150
+ file_path TEXT UNIQUE NOT NULL,
14151
+ task_summary TEXT,
14152
+ what_worked TEXT,
14153
+ what_failed TEXT,
14154
+ learnings TEXT,
14155
+ outcome TEXT,
14156
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
14157
+ indexed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
14158
+ );
14159
+ CREATE TABLE IF NOT EXISTS plans (
14160
+ id TEXT PRIMARY KEY,
14161
+ title TEXT,
14162
+ file_path TEXT UNIQUE NOT NULL,
14163
+ overview TEXT,
14164
+ approach TEXT,
14165
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
14166
+ indexed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
14167
+ );
14168
+ CREATE TABLE IF NOT EXISTS ledgers (
14169
+ id TEXT PRIMARY KEY,
14170
+ session_name TEXT,
14171
+ file_path TEXT UNIQUE NOT NULL,
14172
+ goal TEXT,
14173
+ state_now TEXT,
14174
+ key_decisions TEXT,
14175
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
14176
+ indexed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
14177
+ );
14178
+ CREATE VIRTUAL TABLE IF NOT EXISTS handoffs_fts USING fts5(id, session_name, task_summary, what_worked, what_failed, learnings);
14179
+ CREATE VIRTUAL TABLE IF NOT EXISTS plans_fts USING fts5(id, title, overview, approach);
14180
+ CREATE VIRTUAL TABLE IF NOT EXISTS ledgers_fts USING fts5(id, session_name, goal, state_now, key_decisions);
14181
+ `;
14182
+ }
14183
+ async indexHandoff(record2) {
14184
+ if (!this.db)
14185
+ throw new Error("Database not initialized");
14186
+ const existing = this.db.query(`SELECT id FROM handoffs WHERE file_path = ?`).get(record2.filePath);
14187
+ if (existing) {
14188
+ this.db.run(`DELETE FROM handoffs_fts WHERE id = ?`, [existing.id]);
14189
+ }
14190
+ this.db.run(`
14191
+ INSERT INTO handoffs (id, session_name, file_path, task_summary, what_worked, what_failed, learnings, outcome, indexed_at)
14192
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
14193
+ ON CONFLICT(file_path) DO UPDATE SET
14194
+ id = excluded.id,
14195
+ session_name = excluded.session_name,
14196
+ task_summary = excluded.task_summary,
14197
+ what_worked = excluded.what_worked,
14198
+ what_failed = excluded.what_failed,
14199
+ learnings = excluded.learnings,
14200
+ outcome = excluded.outcome,
14201
+ indexed_at = CURRENT_TIMESTAMP
14202
+ `, [
14203
+ record2.id,
14204
+ record2.sessionName ?? null,
14205
+ record2.filePath,
14206
+ record2.taskSummary ?? null,
14207
+ record2.whatWorked ?? null,
14208
+ record2.whatFailed ?? null,
14209
+ record2.learnings ?? null,
14210
+ record2.outcome ?? null
14211
+ ]);
14212
+ this.db.run(`
14213
+ INSERT INTO handoffs_fts (id, session_name, task_summary, what_worked, what_failed, learnings)
14214
+ VALUES (?, ?, ?, ?, ?, ?)
14215
+ `, [
14216
+ record2.id,
14217
+ record2.sessionName ?? null,
14218
+ record2.taskSummary ?? null,
14219
+ record2.whatWorked ?? null,
14220
+ record2.whatFailed ?? null,
14221
+ record2.learnings ?? null
14222
+ ]);
14223
+ }
14224
+ async indexPlan(record2) {
14225
+ if (!this.db)
14226
+ throw new Error("Database not initialized");
14227
+ const existing = this.db.query(`SELECT id FROM plans WHERE file_path = ?`).get(record2.filePath);
14228
+ if (existing) {
14229
+ this.db.run(`DELETE FROM plans_fts WHERE id = ?`, [existing.id]);
14230
+ }
14231
+ this.db.run(`
14232
+ INSERT INTO plans (id, title, file_path, overview, approach, indexed_at)
14233
+ VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
14234
+ ON CONFLICT(file_path) DO UPDATE SET
14235
+ id = excluded.id,
14236
+ title = excluded.title,
14237
+ overview = excluded.overview,
14238
+ approach = excluded.approach,
14239
+ indexed_at = CURRENT_TIMESTAMP
14240
+ `, [record2.id, record2.title ?? null, record2.filePath, record2.overview ?? null, record2.approach ?? null]);
14241
+ this.db.run(`
14242
+ INSERT INTO plans_fts (id, title, overview, approach)
14243
+ VALUES (?, ?, ?, ?)
14244
+ `, [record2.id, record2.title ?? null, record2.overview ?? null, record2.approach ?? null]);
14245
+ }
14246
+ async indexLedger(record2) {
14247
+ if (!this.db)
14248
+ throw new Error("Database not initialized");
14249
+ const existing = this.db.query(`SELECT id FROM ledgers WHERE file_path = ?`).get(record2.filePath);
14250
+ if (existing) {
14251
+ this.db.run(`DELETE FROM ledgers_fts WHERE id = ?`, [existing.id]);
14252
+ }
14253
+ this.db.run(`
14254
+ INSERT INTO ledgers (id, session_name, file_path, goal, state_now, key_decisions, indexed_at)
14255
+ VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
14256
+ ON CONFLICT(file_path) DO UPDATE SET
14257
+ id = excluded.id,
14258
+ session_name = excluded.session_name,
14259
+ goal = excluded.goal,
14260
+ state_now = excluded.state_now,
14261
+ key_decisions = excluded.key_decisions,
14262
+ indexed_at = CURRENT_TIMESTAMP
14263
+ `, [
14264
+ record2.id,
14265
+ record2.sessionName ?? null,
14266
+ record2.filePath,
14267
+ record2.goal ?? null,
14268
+ record2.stateNow ?? null,
14269
+ record2.keyDecisions ?? null
14270
+ ]);
14271
+ this.db.run(`
14272
+ INSERT INTO ledgers_fts (id, session_name, goal, state_now, key_decisions)
14273
+ VALUES (?, ?, ?, ?, ?)
14274
+ `, [
14275
+ record2.id,
14276
+ record2.sessionName ?? null,
14277
+ record2.goal ?? null,
14278
+ record2.stateNow ?? null,
14279
+ record2.keyDecisions ?? null
14280
+ ]);
14281
+ }
14282
+ async search(query, limit = 10) {
14283
+ if (!this.db)
14284
+ throw new Error("Database not initialized");
14285
+ const results = [];
14286
+ const escapedQuery = this.escapeFtsQuery(query);
14287
+ const handoffs = this.db.query(`
14288
+ SELECT h.id, h.file_path, h.task_summary, rank
14289
+ FROM handoffs_fts
14290
+ JOIN handoffs h ON handoffs_fts.id = h.id
14291
+ WHERE handoffs_fts MATCH ?
14292
+ ORDER BY rank
14293
+ LIMIT ?
14294
+ `).all(escapedQuery, limit);
14295
+ for (const row of handoffs) {
14296
+ results.push({
14297
+ type: "handoff",
14298
+ id: row.id,
14299
+ filePath: row.file_path,
14300
+ summary: row.task_summary,
14301
+ score: -row.rank
14302
+ });
14303
+ }
14304
+ const plans = this.db.query(`
14305
+ SELECT p.id, p.file_path, p.title, rank
14306
+ FROM plans_fts
14307
+ JOIN plans p ON plans_fts.id = p.id
14308
+ WHERE plans_fts MATCH ?
14309
+ ORDER BY rank
14310
+ LIMIT ?
14311
+ `).all(escapedQuery, limit);
14312
+ for (const row of plans) {
14313
+ results.push({
14314
+ type: "plan",
14315
+ id: row.id,
14316
+ filePath: row.file_path,
14317
+ title: row.title,
14318
+ score: -row.rank
14319
+ });
14320
+ }
14321
+ const ledgers = this.db.query(`
14322
+ SELECT l.id, l.file_path, l.session_name, l.goal, rank
14323
+ FROM ledgers_fts
14324
+ JOIN ledgers l ON ledgers_fts.id = l.id
14325
+ WHERE ledgers_fts MATCH ?
14326
+ ORDER BY rank
14327
+ LIMIT ?
14328
+ `).all(escapedQuery, limit);
14329
+ for (const row of ledgers) {
14330
+ results.push({
14331
+ type: "ledger",
14332
+ id: row.id,
14333
+ filePath: row.file_path,
14334
+ title: row.session_name,
14335
+ summary: row.goal,
14336
+ score: -row.rank
14337
+ });
14338
+ }
14339
+ results.sort((a, b) => b.score - a.score);
14340
+ return results.slice(0, limit);
14341
+ }
14342
+ escapeFtsQuery(query) {
14343
+ return query.replace(/['"]/g, "").split(/\s+/).filter((term) => term.length > 0).map((term) => `"${term}"`).join(" OR ");
14344
+ }
14345
+ async close() {
14346
+ if (this.db) {
14347
+ this.db.close();
14348
+ this.db = null;
14349
+ }
14350
+ }
14351
+ }
14352
+ var globalIndex = null;
14353
+ async function getArtifactIndex() {
14354
+ if (!globalIndex) {
14355
+ globalIndex = new ArtifactIndex;
14356
+ await globalIndex.initialize();
14357
+ }
14358
+ return globalIndex;
14359
+ }
14360
+
14361
+ // src/tools/artifact-search.ts
14362
+ var artifact_search = tool({
14363
+ description: `Search past handoffs, plans, and ledgers for relevant precedent.
14364
+ Use this to find:
14365
+ - Similar problems you've solved before
14366
+ - Patterns and approaches that worked
14367
+ - Lessons learned from past sessions
14368
+ Returns ranked results with file paths for further reading.`,
14369
+ args: {
14370
+ query: tool.schema.string().describe("Search query - describe what you're looking for"),
14371
+ limit: tool.schema.number().optional().describe("Max results to return (default: 10)"),
14372
+ type: tool.schema.enum(["all", "handoff", "plan", "ledger"]).optional().describe("Filter by artifact type (default: all)")
14373
+ },
14374
+ execute: async (args) => {
14375
+ try {
14376
+ const index = await getArtifactIndex();
14377
+ const results = await index.search(args.query, args.limit || 10);
14378
+ const filtered = args.type && args.type !== "all" ? results.filter((r) => r.type === args.type) : results;
14379
+ if (filtered.length === 0) {
14380
+ return `No results found for "${args.query}". Try broader search terms.`;
14381
+ }
14382
+ let output = `## Search Results for "${args.query}"
14383
+
14384
+ `;
14385
+ output += `Found ${filtered.length} result(s):
14386
+
14387
+ `;
14388
+ for (const result of filtered) {
14389
+ const typeLabel = result.type.charAt(0).toUpperCase() + result.type.slice(1);
14390
+ output += `### ${typeLabel}: ${result.title || result.id}
14391
+ `;
14392
+ output += `**File:** \`${result.filePath}\`
14393
+ `;
14394
+ if (result.summary) {
14395
+ output += `**Summary:** ${result.summary}
14396
+ `;
14397
+ }
14398
+ output += `**Relevance Score:** ${result.score.toFixed(2)}
14399
+
14400
+ `;
14401
+ }
14402
+ output += `---
14403
+ *Use the Read tool to view full content of relevant files.*`;
14404
+ return output;
14405
+ } catch (e) {
14406
+ return `Error searching artifacts: ${e instanceof Error ? e.message : String(e)}`;
14407
+ }
14408
+ }
14409
+ });
14410
+
13991
14411
  // src/hooks/auto-compact.ts
13992
14412
  function parseTokenLimitError(error45) {
13993
14413
  if (!error45)
@@ -14083,9 +14503,9 @@ function createAutoCompactHook(ctx) {
14083
14503
  }
14084
14504
  }).catch(() => {});
14085
14505
  }
14086
- } catch (e) {
14506
+ } catch (_e) {
14087
14507
  state.retryCount.set(sessionID, retries + 1);
14088
- const delay = Math.min(1000 * Math.pow(2, retries), 1e4);
14508
+ const delay = Math.min(1000 * 2 ** retries, 1e4);
14089
14509
  setTimeout(() => {
14090
14510
  state.inProgress.delete(sessionID);
14091
14511
  attemptRecovery(sessionID, providerID, modelID);
@@ -14167,14 +14587,8 @@ function createAutoCompactHook(ctx) {
14167
14587
 
14168
14588
  // src/hooks/context-injector.ts
14169
14589
  import { readFile } from "fs/promises";
14170
- import { join, dirname, resolve } from "path";
14171
- var ROOT_CONTEXT_FILES = [
14172
- "ARCHITECTURE.md",
14173
- "CODE_STYLE.md",
14174
- "AGENTS.md",
14175
- "CLAUDE.md",
14176
- "README.md"
14177
- ];
14590
+ import { join as join2, dirname as dirname2, resolve } from "path";
14591
+ var ROOT_CONTEXT_FILES = ["ARCHITECTURE.md", "CODE_STYLE.md", "AGENTS.md", "CLAUDE.md", "README.md"];
14178
14592
  var DIRECTORY_CONTEXT_FILES = ["AGENTS.md", "README.md"];
14179
14593
  var FILE_ACCESS_TOOLS = ["Read", "read", "Edit", "edit"];
14180
14594
  var CACHE_TTL = 30000;
@@ -14193,7 +14607,7 @@ function createContextInjectorHook(ctx) {
14193
14607
  cache.lastRootCheck = now;
14194
14608
  for (const filename of ROOT_CONTEXT_FILES) {
14195
14609
  try {
14196
- const filepath = join(ctx.directory, filename);
14610
+ const filepath = join2(ctx.directory, filename);
14197
14611
  const content = await readFile(filepath, "utf-8");
14198
14612
  if (content.trim()) {
14199
14613
  cache.rootContent.set(filename, content);
@@ -14205,15 +14619,15 @@ function createContextInjectorHook(ctx) {
14205
14619
  async function walkUpForContextFiles(filePath) {
14206
14620
  const absPath = resolve(filePath);
14207
14621
  const projectRoot = resolve(ctx.directory);
14208
- const cacheKey = dirname(absPath);
14622
+ const cacheKey = dirname2(absPath);
14209
14623
  if (cache.directoryContent.has(cacheKey)) {
14210
14624
  return cache.directoryContent.get(cacheKey);
14211
14625
  }
14212
14626
  const collected = new Map;
14213
- let currentDir = dirname(absPath);
14627
+ let currentDir = dirname2(absPath);
14214
14628
  while (currentDir.startsWith(projectRoot) || currentDir === projectRoot) {
14215
14629
  for (const filename of DIRECTORY_CONTEXT_FILES) {
14216
- const contextPath = join(currentDir, filename);
14630
+ const contextPath = join2(currentDir, filename);
14217
14631
  const relPath = currentDir.replace(projectRoot, "").replace(/^\//, "") || ".";
14218
14632
  const key = `${relPath}/${filename}`;
14219
14633
  if (!collected.has(key)) {
@@ -14227,7 +14641,7 @@ function createContextInjectorHook(ctx) {
14227
14641
  }
14228
14642
  if (currentDir === projectRoot)
14229
14643
  break;
14230
- const parent = dirname(currentDir);
14644
+ const parent = dirname2(currentDir);
14231
14645
  if (parent === currentDir)
14232
14646
  break;
14233
14647
  currentDir = parent;
@@ -14288,144 +14702,6 @@ ${blocks.join(`
14288
14702
  };
14289
14703
  }
14290
14704
 
14291
- // src/hooks/preemptive-compaction.ts
14292
- var MODEL_CONTEXT_LIMITS = {
14293
- "claude-opus": 200000,
14294
- "claude-sonnet": 200000,
14295
- "claude-haiku": 200000,
14296
- "claude-3": 200000,
14297
- "claude-4": 200000,
14298
- "gpt-4o": 128000,
14299
- "gpt-4-turbo": 128000,
14300
- "gpt-4": 128000,
14301
- "gpt-5": 200000,
14302
- o1: 200000,
14303
- o3: 200000,
14304
- gemini: 1e6
14305
- };
14306
- var DEFAULT_CONTEXT_LIMIT = 200000;
14307
- var DEFAULT_THRESHOLD = 0.8;
14308
- var MIN_TOKENS_FOR_COMPACTION = 50000;
14309
- var COMPACTION_COOLDOWN_MS = 60000;
14310
- function getContextLimit(modelID) {
14311
- const modelLower = modelID.toLowerCase();
14312
- for (const [pattern, limit] of Object.entries(MODEL_CONTEXT_LIMITS)) {
14313
- if (modelLower.includes(pattern)) {
14314
- return limit;
14315
- }
14316
- }
14317
- return DEFAULT_CONTEXT_LIMIT;
14318
- }
14319
- function createPreemptiveCompactionHook(ctx) {
14320
- const state = {
14321
- lastCompactionTime: new Map,
14322
- compactionInProgress: new Set
14323
- };
14324
- async function checkAndCompact(sessionID, providerID, modelID) {
14325
- if (state.compactionInProgress.has(sessionID))
14326
- return;
14327
- const lastTime = state.lastCompactionTime.get(sessionID) || 0;
14328
- if (Date.now() - lastTime < COMPACTION_COOLDOWN_MS)
14329
- return;
14330
- try {
14331
- const resp = await ctx.client.session.messages({
14332
- path: { id: sessionID },
14333
- query: { directory: ctx.directory }
14334
- });
14335
- const messages = resp.data;
14336
- if (!Array.isArray(messages) || messages.length === 0)
14337
- return;
14338
- const lastAssistant = [...messages].reverse().find((m) => {
14339
- const msg = m;
14340
- const info2 = msg.info;
14341
- return info2?.role === "assistant";
14342
- });
14343
- if (!lastAssistant)
14344
- return;
14345
- const info = lastAssistant.info;
14346
- const usage = info?.usage;
14347
- const inputTokens = usage?.inputTokens || 0;
14348
- const cacheRead = usage?.cacheReadInputTokens || 0;
14349
- const totalUsed = inputTokens + cacheRead;
14350
- if (totalUsed < MIN_TOKENS_FOR_COMPACTION)
14351
- return;
14352
- const model = modelID || info?.modelID || "";
14353
- const contextLimit = getContextLimit(model);
14354
- const usageRatio = totalUsed / contextLimit;
14355
- if (usageRatio < DEFAULT_THRESHOLD)
14356
- return;
14357
- const lastUserMsg = [...messages].reverse().find((m) => {
14358
- const msg = m;
14359
- const msgInfo = msg.info;
14360
- return msgInfo?.role === "user";
14361
- });
14362
- if (lastUserMsg) {
14363
- const parts = lastUserMsg.parts;
14364
- const text = parts?.find((p) => p.type === "text")?.text || "";
14365
- if (text.includes("summarized") || text.includes("compacted"))
14366
- return;
14367
- }
14368
- state.compactionInProgress.add(sessionID);
14369
- state.lastCompactionTime.set(sessionID, Date.now());
14370
- await ctx.client.tui.showToast({
14371
- body: {
14372
- title: "Context Window",
14373
- message: `${Math.round(usageRatio * 100)}% used - auto-compacting...`,
14374
- variant: "warning",
14375
- duration: 3000
14376
- }
14377
- }).catch(() => {});
14378
- const provider = providerID || info?.providerID;
14379
- const modelToUse = modelID || info?.modelID;
14380
- if (provider && modelToUse) {
14381
- await ctx.client.session.summarize({
14382
- path: { id: sessionID },
14383
- body: { providerID: provider, modelID: modelToUse },
14384
- query: { directory: ctx.directory }
14385
- });
14386
- await ctx.client.tui.showToast({
14387
- body: {
14388
- title: "Compacted",
14389
- message: "Session summarized successfully",
14390
- variant: "success",
14391
- duration: 3000
14392
- }
14393
- }).catch(() => {});
14394
- }
14395
- } catch (e) {} finally {
14396
- state.compactionInProgress.delete(sessionID);
14397
- }
14398
- }
14399
- return {
14400
- event: async ({ event }) => {
14401
- const props = event.properties;
14402
- if (event.type === "session.deleted") {
14403
- const sessionInfo = props?.info;
14404
- if (sessionInfo?.id) {
14405
- state.lastCompactionTime.delete(sessionInfo.id);
14406
- state.compactionInProgress.delete(sessionInfo.id);
14407
- }
14408
- return;
14409
- }
14410
- if (event.type === "message.updated") {
14411
- const info = props?.info;
14412
- const sessionID = info?.sessionID;
14413
- if (sessionID && info?.role === "assistant") {
14414
- const providerID = info.providerID;
14415
- const modelID = info.modelID;
14416
- await checkAndCompact(sessionID, providerID, modelID);
14417
- }
14418
- }
14419
- if (event.type === "session.idle") {
14420
- const sessionID = props?.sessionID;
14421
- if (sessionID) {
14422
- await checkAndCompact(sessionID);
14423
- }
14424
- }
14425
- }
14426
- };
14427
- }
14428
-
14429
14705
  // src/hooks/session-recovery.ts
14430
14706
  var RECOVERABLE_ERRORS = {
14431
14707
  TOOL_RESULT_MISSING: "tool_result block(s) missing",
@@ -14608,15 +14884,9 @@ function createSessionRecoveryHook(ctx) {
14608
14884
  }
14609
14885
 
14610
14886
  // src/hooks/token-aware-truncation.ts
14611
- var TRUNCATABLE_TOOLS = [
14612
- "grep",
14613
- "Grep",
14614
- "glob",
14615
- "Glob",
14616
- "ast_grep_search"
14617
- ];
14887
+ var TRUNCATABLE_TOOLS = ["grep", "Grep", "glob", "Glob", "ast_grep_search"];
14618
14888
  var CHARS_PER_TOKEN = 4;
14619
- var DEFAULT_CONTEXT_LIMIT2 = 200000;
14889
+ var DEFAULT_CONTEXT_LIMIT = 200000;
14620
14890
  var DEFAULT_MAX_OUTPUT_TOKENS = 50000;
14621
14891
  var SAFETY_MARGIN = 0.5;
14622
14892
  var PRESERVE_HEADER_LINES = 3;
@@ -14677,7 +14947,7 @@ function createTokenAwareTruncationHook(ctx) {
14677
14947
  });
14678
14948
  const messages = resp.data;
14679
14949
  if (!Array.isArray(messages) || messages.length === 0) {
14680
- return { used: 0, limit: DEFAULT_CONTEXT_LIMIT2 };
14950
+ return { used: 0, limit: DEFAULT_CONTEXT_LIMIT };
14681
14951
  }
14682
14952
  const lastAssistant = [...messages].reverse().find((m) => {
14683
14953
  const msg = m;
@@ -14685,19 +14955,19 @@ function createTokenAwareTruncationHook(ctx) {
14685
14955
  return info2?.role === "assistant";
14686
14956
  });
14687
14957
  if (!lastAssistant) {
14688
- return { used: 0, limit: DEFAULT_CONTEXT_LIMIT2 };
14958
+ return { used: 0, limit: DEFAULT_CONTEXT_LIMIT };
14689
14959
  }
14690
14960
  const info = lastAssistant.info;
14691
14961
  const usage = info?.usage;
14692
14962
  const inputTokens = usage?.inputTokens || 0;
14693
14963
  const cacheRead = usage?.cacheReadInputTokens || 0;
14694
14964
  const used = inputTokens + cacheRead;
14695
- const limit = DEFAULT_CONTEXT_LIMIT2;
14965
+ const limit = DEFAULT_CONTEXT_LIMIT;
14696
14966
  const result = { used, limit };
14697
14967
  state.sessionTokenUsage.set(sessionID, result);
14698
14968
  return result;
14699
14969
  } catch {
14700
- return state.sessionTokenUsage.get(sessionID) || { used: 0, limit: DEFAULT_CONTEXT_LIMIT2 };
14970
+ return state.sessionTokenUsage.get(sessionID) || { used: 0, limit: DEFAULT_CONTEXT_LIMIT };
14701
14971
  }
14702
14972
  }
14703
14973
  function calculateMaxOutputTokens(used, limit) {
@@ -14757,8 +15027,8 @@ function createTokenAwareTruncationHook(ctx) {
14757
15027
  // src/hooks/context-window-monitor.ts
14758
15028
  var WARNING_THRESHOLD = 0.7;
14759
15029
  var CRITICAL_THRESHOLD = 0.85;
14760
- var DEFAULT_CONTEXT_LIMIT3 = 200000;
14761
- var MODEL_CONTEXT_LIMITS2 = {
15030
+ var DEFAULT_CONTEXT_LIMIT2 = 200000;
15031
+ var MODEL_CONTEXT_LIMITS = {
14762
15032
  "claude-opus": 200000,
14763
15033
  "claude-sonnet": 200000,
14764
15034
  "claude-haiku": 200000,
@@ -14768,14 +15038,14 @@ var MODEL_CONTEXT_LIMITS2 = {
14768
15038
  o3: 200000,
14769
15039
  gemini: 1e6
14770
15040
  };
14771
- function getContextLimit2(modelID) {
15041
+ function getContextLimit(modelID) {
14772
15042
  const modelLower = modelID.toLowerCase();
14773
- for (const [pattern, limit] of Object.entries(MODEL_CONTEXT_LIMITS2)) {
15043
+ for (const [pattern, limit] of Object.entries(MODEL_CONTEXT_LIMITS)) {
14774
15044
  if (modelLower.includes(pattern)) {
14775
15045
  return limit;
14776
15046
  }
14777
15047
  }
14778
- return DEFAULT_CONTEXT_LIMIT3;
15048
+ return DEFAULT_CONTEXT_LIMIT2;
14779
15049
  }
14780
15050
  var WARNING_COOLDOWN_MS = 120000;
14781
15051
  function createContextWindowMonitorHook(ctx) {
@@ -14825,7 +15095,7 @@ function createContextWindowMonitorHook(ctx) {
14825
15095
  const cacheRead = usage?.cacheReadInputTokens || 0;
14826
15096
  const totalUsed = inputTokens + cacheRead;
14827
15097
  const modelID = info.modelID || "";
14828
- const contextLimit = getContextLimit2(modelID);
15098
+ const contextLimit = getContextLimit(modelID);
14829
15099
  const usageRatio = totalUsed / contextLimit;
14830
15100
  state.lastUsageRatio.set(sessionID, usageRatio);
14831
15101
  if (usageRatio >= WARNING_THRESHOLD) {
@@ -14907,7 +15177,7 @@ function analyzeComments(content) {
14907
15177
  }
14908
15178
  return issues;
14909
15179
  }
14910
- function createCommentCheckerHook(ctx) {
15180
+ function createCommentCheckerHook(_ctx) {
14911
15181
  return {
14912
15182
  "tool.execute.after": async (input, output) => {
14913
15183
  if (input.tool !== "Edit" && input.tool !== "edit")
@@ -14933,6 +15203,373 @@ Comments should explain WHY, not WHAT. Consider removing obvious comments.`;
14933
15203
  };
14934
15204
  }
14935
15205
 
15206
+ // src/hooks/ledger-loader.ts
15207
+ import { readFile as readFile2, readdir } from "fs/promises";
15208
+ import { join as join3 } from "path";
15209
+ var LEDGER_DIR = "thoughts/ledgers";
15210
+ var LEDGER_PREFIX = "CONTINUITY_";
15211
+ async function findCurrentLedger(directory) {
15212
+ const ledgerDir = join3(directory, LEDGER_DIR);
15213
+ try {
15214
+ const files = await readdir(ledgerDir);
15215
+ const ledgerFiles = files.filter((f) => f.startsWith(LEDGER_PREFIX) && f.endsWith(".md"));
15216
+ if (ledgerFiles.length === 0)
15217
+ return null;
15218
+ let latestFile = ledgerFiles[0];
15219
+ let latestMtime = 0;
15220
+ for (const file2 of ledgerFiles) {
15221
+ const filePath2 = join3(ledgerDir, file2);
15222
+ try {
15223
+ const stat = await Bun.file(filePath2).stat();
15224
+ if (stat && stat.mtime.getTime() > latestMtime) {
15225
+ latestMtime = stat.mtime.getTime();
15226
+ latestFile = file2;
15227
+ }
15228
+ } catch {}
15229
+ }
15230
+ const filePath = join3(ledgerDir, latestFile);
15231
+ const content = await readFile2(filePath, "utf-8");
15232
+ const sessionName = latestFile.replace(LEDGER_PREFIX, "").replace(".md", "");
15233
+ return { sessionName, filePath, content };
15234
+ } catch {
15235
+ return null;
15236
+ }
15237
+ }
15238
+ function formatLedgerInjection(ledger) {
15239
+ return `<continuity-ledger session="${ledger.sessionName}">
15240
+ ${ledger.content}
15241
+ </continuity-ledger>
15242
+
15243
+ You are resuming work from a previous context clear. The ledger above contains your session state.
15244
+ Review it and continue from where you left off. The "Now" item is your current focus.`;
15245
+ }
15246
+ function createLedgerLoaderHook(ctx) {
15247
+ return {
15248
+ "chat.params": async (_input, output) => {
15249
+ const ledger = await findCurrentLedger(ctx.directory);
15250
+ if (!ledger)
15251
+ return;
15252
+ const injection = formatLedgerInjection(ledger);
15253
+ if (output.system) {
15254
+ output.system = `${injection}
15255
+
15256
+ ${output.system}`;
15257
+ } else {
15258
+ output.system = injection;
15259
+ }
15260
+ }
15261
+ };
15262
+ }
15263
+
15264
+ // src/hooks/auto-clear-ledger.ts
15265
+ var MODEL_CONTEXT_LIMITS2 = {
15266
+ "claude-opus": 200000,
15267
+ "claude-sonnet": 200000,
15268
+ "claude-haiku": 200000,
15269
+ "claude-3": 200000,
15270
+ "claude-4": 200000,
15271
+ "gpt-4o": 128000,
15272
+ "gpt-4-turbo": 128000,
15273
+ "gpt-4": 128000,
15274
+ "gpt-5": 200000,
15275
+ o1: 200000,
15276
+ o3: 200000,
15277
+ gemini: 1e6
15278
+ };
15279
+ var DEFAULT_CONTEXT_LIMIT3 = 200000;
15280
+ var DEFAULT_THRESHOLD = 0.8;
15281
+ var MIN_TOKENS_FOR_CLEAR = 50000;
15282
+ var CLEAR_COOLDOWN_MS = 60000;
15283
+ function getContextLimit2(modelID) {
15284
+ const modelLower = modelID.toLowerCase();
15285
+ for (const [pattern, limit] of Object.entries(MODEL_CONTEXT_LIMITS2)) {
15286
+ if (modelLower.includes(pattern)) {
15287
+ return limit;
15288
+ }
15289
+ }
15290
+ return DEFAULT_CONTEXT_LIMIT3;
15291
+ }
15292
+ function createAutoClearLedgerHook(ctx) {
15293
+ const state = {
15294
+ lastClearTime: new Map,
15295
+ clearInProgress: new Set
15296
+ };
15297
+ async function checkAndClear(sessionID, _providerID, modelID) {
15298
+ if (state.clearInProgress.has(sessionID))
15299
+ return;
15300
+ const lastTime = state.lastClearTime.get(sessionID) || 0;
15301
+ if (Date.now() - lastTime < CLEAR_COOLDOWN_MS)
15302
+ return;
15303
+ try {
15304
+ const resp = await ctx.client.session.messages({
15305
+ path: { id: sessionID },
15306
+ query: { directory: ctx.directory }
15307
+ });
15308
+ const messages = resp.data;
15309
+ if (!Array.isArray(messages) || messages.length === 0)
15310
+ return;
15311
+ const lastAssistant = [...messages].reverse().find((m) => {
15312
+ const msg = m;
15313
+ const info2 = msg.info;
15314
+ return info2?.role === "assistant";
15315
+ });
15316
+ if (!lastAssistant)
15317
+ return;
15318
+ const info = lastAssistant.info;
15319
+ const usage = info?.usage;
15320
+ const inputTokens = usage?.inputTokens || 0;
15321
+ const cacheRead = usage?.cacheReadInputTokens || 0;
15322
+ const totalUsed = inputTokens + cacheRead;
15323
+ if (totalUsed < MIN_TOKENS_FOR_CLEAR)
15324
+ return;
15325
+ const model = modelID || info?.modelID || "";
15326
+ const contextLimit = getContextLimit2(model);
15327
+ const usageRatio = totalUsed / contextLimit;
15328
+ if (usageRatio < DEFAULT_THRESHOLD)
15329
+ return;
15330
+ state.clearInProgress.add(sessionID);
15331
+ state.lastClearTime.set(sessionID, Date.now());
15332
+ await ctx.client.tui.showToast({
15333
+ body: {
15334
+ title: "Context Window",
15335
+ message: `${Math.round(usageRatio * 100)}% used - saving ledger and clearing...`,
15336
+ variant: "warning",
15337
+ duration: 3000
15338
+ }
15339
+ }).catch(() => {});
15340
+ const ledgerSessionResp = await ctx.client.session.create({
15341
+ body: {},
15342
+ query: { directory: ctx.directory }
15343
+ });
15344
+ const ledgerSessionID = ledgerSessionResp.data?.id;
15345
+ if (ledgerSessionID) {
15346
+ await ctx.client.session.prompt({
15347
+ path: { id: ledgerSessionID },
15348
+ body: {
15349
+ parts: [
15350
+ { type: "text", text: "Update the continuity ledger with current session state before context clear." }
15351
+ ],
15352
+ agent: "ledger-creator"
15353
+ },
15354
+ query: { directory: ctx.directory }
15355
+ });
15356
+ let attempts = 0;
15357
+ while (attempts < 30) {
15358
+ await new Promise((resolve2) => setTimeout(resolve2, 2000));
15359
+ const statusResp = await ctx.client.session.get({
15360
+ path: { id: ledgerSessionID },
15361
+ query: { directory: ctx.directory }
15362
+ });
15363
+ if (statusResp.data?.status === "idle") {
15364
+ break;
15365
+ }
15366
+ attempts++;
15367
+ }
15368
+ }
15369
+ const handoffSessionResp = await ctx.client.session.create({
15370
+ body: {},
15371
+ query: { directory: ctx.directory }
15372
+ });
15373
+ const handoffSessionID = handoffSessionResp.data?.id;
15374
+ if (handoffSessionID) {
15375
+ await ctx.client.session.prompt({
15376
+ path: { id: handoffSessionID },
15377
+ body: {
15378
+ parts: [
15379
+ {
15380
+ type: "text",
15381
+ text: "Create a handoff document. Read the current ledger at thoughts/ledgers/ for context."
15382
+ }
15383
+ ],
15384
+ agent: "handoff-creator"
15385
+ },
15386
+ query: { directory: ctx.directory }
15387
+ });
15388
+ let attempts = 0;
15389
+ while (attempts < 30) {
15390
+ await new Promise((resolve2) => setTimeout(resolve2, 2000));
15391
+ const statusResp = await ctx.client.session.get({
15392
+ path: { id: handoffSessionID },
15393
+ query: { directory: ctx.directory }
15394
+ });
15395
+ if (statusResp.data?.status === "idle") {
15396
+ break;
15397
+ }
15398
+ attempts++;
15399
+ }
15400
+ }
15401
+ const firstMessage = messages[0];
15402
+ const firstMessageID = firstMessage?.info?.id;
15403
+ if (!firstMessageID) {
15404
+ throw new Error("Could not find first message ID for revert");
15405
+ }
15406
+ await ctx.client.session.revert({
15407
+ path: { id: sessionID },
15408
+ body: { messageID: firstMessageID },
15409
+ query: { directory: ctx.directory }
15410
+ });
15411
+ const ledger = await findCurrentLedger(ctx.directory);
15412
+ if (ledger) {
15413
+ const injection = formatLedgerInjection(ledger);
15414
+ await ctx.client.session.prompt({
15415
+ path: { id: sessionID },
15416
+ body: {
15417
+ parts: [{ type: "text", text: injection }],
15418
+ noReply: true
15419
+ },
15420
+ query: { directory: ctx.directory }
15421
+ });
15422
+ }
15423
+ await ctx.client.tui.showToast({
15424
+ body: {
15425
+ title: "Context Cleared",
15426
+ message: "Ledger + handoff saved. Session ready to continue.",
15427
+ variant: "success",
15428
+ duration: 5000
15429
+ }
15430
+ }).catch(() => {});
15431
+ } catch (e) {
15432
+ console.error("[auto-clear-ledger] Error:", e);
15433
+ await ctx.client.tui.showToast({
15434
+ body: {
15435
+ title: "Clear Failed",
15436
+ message: "Could not complete context clear. Continuing normally.",
15437
+ variant: "error",
15438
+ duration: 5000
15439
+ }
15440
+ }).catch(() => {});
15441
+ } finally {
15442
+ state.clearInProgress.delete(sessionID);
15443
+ }
15444
+ }
15445
+ return {
15446
+ event: async ({ event }) => {
15447
+ const props = event.properties;
15448
+ if (event.type === "session.deleted") {
15449
+ const sessionInfo = props?.info;
15450
+ if (sessionInfo?.id) {
15451
+ state.lastClearTime.delete(sessionInfo.id);
15452
+ state.clearInProgress.delete(sessionInfo.id);
15453
+ }
15454
+ return;
15455
+ }
15456
+ if (event.type === "message.updated") {
15457
+ const info = props?.info;
15458
+ const sessionID = info?.sessionID;
15459
+ if (sessionID && info?.role === "assistant") {
15460
+ const providerID = info.providerID;
15461
+ const modelID = info.modelID;
15462
+ await checkAndClear(sessionID, providerID, modelID);
15463
+ }
15464
+ }
15465
+ if (event.type === "session.idle") {
15466
+ const sessionID = props?.sessionID;
15467
+ if (sessionID) {
15468
+ await checkAndClear(sessionID);
15469
+ }
15470
+ }
15471
+ }
15472
+ };
15473
+ }
15474
+
15475
+ // src/hooks/artifact-auto-index.ts
15476
+ import { readFileSync as readFileSync3 } from "fs";
15477
+ var LEDGER_PATH_PATTERN = /thoughts\/ledgers\/CONTINUITY_(.+)\.md$/;
15478
+ var HANDOFF_PATH_PATTERN = /thoughts\/shared\/handoffs\/(.+)\.md$/;
15479
+ var PLAN_PATH_PATTERN = /thoughts\/shared\/plans\/(.+)\.md$/;
15480
+ function parseLedger(content, filePath, sessionName) {
15481
+ const goalMatch = content.match(/## Goal\n([^\n]+)/);
15482
+ const stateMatch = content.match(/- Now: ([^\n]+)/);
15483
+ const decisionsMatch = content.match(/## Key Decisions\n([\s\S]*?)(?=\n## |$)/);
15484
+ return {
15485
+ id: `ledger-${sessionName}`,
15486
+ sessionName,
15487
+ filePath,
15488
+ goal: goalMatch?.[1] || "",
15489
+ stateNow: stateMatch?.[1] || "",
15490
+ keyDecisions: decisionsMatch?.[1]?.trim() || ""
15491
+ };
15492
+ }
15493
+ function parseHandoff(content, filePath, fileName) {
15494
+ const sessionMatch = content.match(/^session:\s*(.+)$/m);
15495
+ const sessionName = sessionMatch?.[1] || fileName;
15496
+ const taskMatch = content.match(/\*\*Working on:\*\*\s*([^\n]+)/);
15497
+ const taskSummary = taskMatch?.[1] || "";
15498
+ const learningsMatch = content.match(/## Learnings\n\n([\s\S]*?)(?=\n## |$)/);
15499
+ const learnings = learningsMatch?.[1]?.trim() || "";
15500
+ const workedMatch = content.match(/## What Worked\n\n([\s\S]*?)(?=\n## |$)/);
15501
+ const whatWorked = workedMatch?.[1]?.trim() || learnings;
15502
+ const failedMatch = content.match(/## What Failed\n\n([\s\S]*?)(?=\n## |$)/);
15503
+ const whatFailed = failedMatch?.[1]?.trim() || "";
15504
+ return {
15505
+ id: `handoff-${fileName}`,
15506
+ sessionName,
15507
+ filePath,
15508
+ taskSummary,
15509
+ whatWorked,
15510
+ whatFailed,
15511
+ learnings,
15512
+ outcome: "UNKNOWN"
15513
+ };
15514
+ }
15515
+ function parsePlan(content, filePath, fileName) {
15516
+ const titleMatch = content.match(/^# (.+)$/m);
15517
+ const title = titleMatch?.[1] || fileName;
15518
+ const overviewMatch = content.match(/## Overview\n\n([\s\S]*?)(?=\n## |$)/);
15519
+ const overview = overviewMatch?.[1]?.trim() || "";
15520
+ const approachMatch = content.match(/## Approach\n\n([\s\S]*?)(?=\n## |$)/);
15521
+ const approach = approachMatch?.[1]?.trim() || "";
15522
+ return {
15523
+ id: `plan-${fileName}`,
15524
+ title,
15525
+ filePath,
15526
+ overview,
15527
+ approach
15528
+ };
15529
+ }
15530
+ function createArtifactAutoIndexHook(_ctx) {
15531
+ return {
15532
+ "tool.execute.after": async (input, _output) => {
15533
+ if (input.tool !== "write")
15534
+ return;
15535
+ const filePath = input.args?.filePath;
15536
+ if (!filePath)
15537
+ return;
15538
+ try {
15539
+ const ledgerMatch = filePath.match(LEDGER_PATH_PATTERN);
15540
+ if (ledgerMatch) {
15541
+ const content = readFileSync3(filePath, "utf-8");
15542
+ const index = await getArtifactIndex();
15543
+ const record2 = parseLedger(content, filePath, ledgerMatch[1]);
15544
+ await index.indexLedger(record2);
15545
+ console.log(`[artifact-auto-index] Indexed ledger: ${filePath}`);
15546
+ return;
15547
+ }
15548
+ const handoffMatch = filePath.match(HANDOFF_PATH_PATTERN);
15549
+ if (handoffMatch) {
15550
+ const content = readFileSync3(filePath, "utf-8");
15551
+ const index = await getArtifactIndex();
15552
+ const record2 = parseHandoff(content, filePath, handoffMatch[1]);
15553
+ await index.indexHandoff(record2);
15554
+ console.log(`[artifact-auto-index] Indexed handoff: ${filePath}`);
15555
+ return;
15556
+ }
15557
+ const planMatch = filePath.match(PLAN_PATH_PATTERN);
15558
+ if (planMatch) {
15559
+ const content = readFileSync3(filePath, "utf-8");
15560
+ const index = await getArtifactIndex();
15561
+ const record2 = parsePlan(content, filePath, planMatch[1]);
15562
+ await index.indexPlan(record2);
15563
+ console.log(`[artifact-auto-index] Indexed plan: ${filePath}`);
15564
+ return;
15565
+ }
15566
+ } catch (e) {
15567
+ console.error(`[artifact-auto-index] Error indexing ${filePath}:`, e);
15568
+ }
15569
+ }
15570
+ };
15571
+ }
15572
+
14936
15573
  // src/tools/background-task/manager.ts
14937
15574
  var POLL_INTERVAL_MS = 2000;
14938
15575
  function generateTaskId() {
@@ -15103,7 +15740,7 @@ ${task.error}
15103
15740
  `;
15104
15741
  }
15105
15742
  if (task.progress?.lastMessage) {
15106
- const preview = task.progress.lastMessage.length > 200 ? task.progress.lastMessage.slice(0, 200) + "..." : task.progress.lastMessage;
15743
+ const preview = task.progress.lastMessage.length > 200 ? `${task.progress.lastMessage.slice(0, 200)}...` : task.progress.lastMessage;
15107
15744
  output += `
15108
15745
  ### Last Message Preview
15109
15746
  ${preview}
@@ -15343,6 +15980,18 @@ var MCP_SERVERS = {
15343
15980
  command: ["npx", "-y", "@upstash/context7-mcp@latest"]
15344
15981
  }
15345
15982
  };
15983
+ if (process.env.PERPLEXITY_API_KEY) {
15984
+ MCP_SERVERS.perplexity = {
15985
+ type: "local",
15986
+ command: ["npx", "-y", "@anthropic/mcp-perplexity"]
15987
+ };
15988
+ }
15989
+ if (process.env.FIRECRAWL_API_KEY) {
15990
+ MCP_SERVERS.firecrawl = {
15991
+ type: "local",
15992
+ command: ["npx", "-y", "firecrawl-mcp"]
15993
+ };
15994
+ }
15346
15995
  var OpenCodeConfigPlugin = async (ctx) => {
15347
15996
  const astGrepStatus = await checkAstGrepAvailable();
15348
15997
  if (!astGrepStatus.available) {
@@ -15351,11 +16000,13 @@ var OpenCodeConfigPlugin = async (ctx) => {
15351
16000
  const thinkModeState = new Map;
15352
16001
  const autoCompactHook = createAutoCompactHook(ctx);
15353
16002
  const contextInjectorHook = createContextInjectorHook(ctx);
15354
- const preemptiveCompactionHook = createPreemptiveCompactionHook(ctx);
16003
+ const autoClearLedgerHook = createAutoClearLedgerHook(ctx);
16004
+ const ledgerLoaderHook = createLedgerLoaderHook(ctx);
15355
16005
  const sessionRecoveryHook = createSessionRecoveryHook(ctx);
15356
16006
  const tokenAwareTruncationHook = createTokenAwareTruncationHook(ctx);
15357
16007
  const contextWindowMonitorHook = createContextWindowMonitorHook(ctx);
15358
16008
  const commentCheckerHook = createCommentCheckerHook(ctx);
16009
+ const artifactAutoIndexHook = createArtifactAutoIndexHook(ctx);
15359
16010
  const backgroundTaskManager = new BackgroundTaskManager(ctx);
15360
16011
  const backgroundTaskTools = createBackgroundTaskTools(backgroundTaskManager);
15361
16012
  return {
@@ -15363,6 +16014,7 @@ var OpenCodeConfigPlugin = async (ctx) => {
15363
16014
  ast_grep_search,
15364
16015
  ast_grep_replace,
15365
16016
  look_at,
16017
+ artifact_search,
15366
16018
  ...backgroundTaskTools
15367
16019
  },
15368
16020
  config: async (config2) => {
@@ -15391,6 +16043,16 @@ var OpenCodeConfigPlugin = async (ctx) => {
15391
16043
  description: "Initialize project with ARCHITECTURE.md and CODE_STYLE.md",
15392
16044
  agent: "project-initializer",
15393
16045
  template: `Initialize this project. $ARGUMENTS`
16046
+ },
16047
+ ledger: {
16048
+ description: "Create or update continuity ledger for session state",
16049
+ agent: "ledger-creator",
16050
+ template: `Update the continuity ledger. $ARGUMENTS`
16051
+ },
16052
+ search: {
16053
+ description: "Search past handoffs, plans, and ledgers",
16054
+ agent: "artifact-searcher",
16055
+ template: `Search for: $ARGUMENTS`
15394
16056
  }
15395
16057
  };
15396
16058
  },
@@ -15399,6 +16061,7 @@ var OpenCodeConfigPlugin = async (ctx) => {
15399
16061
  thinkModeState.set(input.sessionID, detectThinkKeyword(text));
15400
16062
  },
15401
16063
  "chat.params": async (input, output) => {
16064
+ await ledgerLoaderHook["chat.params"](input, output);
15402
16065
  await contextInjectorHook["chat.params"](input, output);
15403
16066
  await contextWindowMonitorHook["chat.params"](input, output);
15404
16067
  if (thinkModeState.get(input.sessionID)) {
@@ -15415,6 +16078,7 @@ var OpenCodeConfigPlugin = async (ctx) => {
15415
16078
  await tokenAwareTruncationHook["tool.execute.after"]({ name: input.tool, sessionID: input.sessionID }, output);
15416
16079
  await commentCheckerHook["tool.execute.after"]({ tool: input.tool, args: input.args }, output);
15417
16080
  await contextInjectorHook["tool.execute.after"]({ tool: input.tool, args: input.args }, output);
16081
+ await artifactAutoIndexHook["tool.execute.after"]({ tool: input.tool, args: input.args }, output);
15418
16082
  },
15419
16083
  event: async ({ event }) => {
15420
16084
  if (event.type === "session.deleted") {
@@ -15424,7 +16088,7 @@ var OpenCodeConfigPlugin = async (ctx) => {
15424
16088
  }
15425
16089
  }
15426
16090
  await autoCompactHook.event({ event });
15427
- await preemptiveCompactionHook.event({ event });
16091
+ await autoClearLedgerHook.event({ event });
15428
16092
  await sessionRecoveryHook.event({ event });
15429
16093
  await tokenAwareTruncationHook.event({ event });
15430
16094
  await contextWindowMonitorHook.event({ event });