@ouro.bot/cli 0.1.0-alpha.206 → 0.1.0-alpha.208

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.
@@ -1,2027 +1,38 @@
1
1
  "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
2
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.restTool = exports.settleTool = exports.observeTool = exports.ponderTool = exports.tools = exports.baseToolDefinitions = exports.editFileReadTracker = void 0;
37
- exports.renderInnerProgressStatus = renderInnerProgressStatus;
38
- const fs = __importStar(require("fs"));
39
- const fg = __importStar(require("fast-glob"));
40
- const child_process_1 = require("child_process");
41
- const path = __importStar(require("path"));
42
- const skills_1 = require("./skills");
43
- const shell_sessions_1 = require("./shell-sessions");
44
- const config_1 = require("../heart/config");
45
- const runtime_1 = require("../nerves/runtime");
46
- const file_state_1 = require("../mind/file-state");
47
- const scrutiny_1 = require("../mind/scrutiny");
48
- const identity_1 = require("../heart/identity");
49
- const identity_2 = require("../heart/identity");
50
- const config_registry_1 = require("../heart/config-registry");
51
- const socket_client_1 = require("../heart/daemon/socket-client");
52
- const thoughts_1 = require("../heart/daemon/thoughts");
53
- const manager_1 = require("../heart/bridges/manager");
54
- const session_recall_1 = require("../heart/session-recall");
55
- const session_activity_1 = require("../heart/session-activity");
56
- const active_work_1 = require("../heart/active-work");
3
+ exports.tools = exports.baseToolDefinitions = exports.editFileReadTracker = exports.renderInnerProgressStatus = exports.restTool = exports.settleTool = exports.observeTool = exports.ponderTool = void 0;
4
+ const tools_files_1 = require("./tools-files");
5
+ const tools_shell_1 = require("./tools-shell");
6
+ const tools_memory_1 = require("./tools-memory");
7
+ const tools_bridge_1 = require("./tools-bridge");
8
+ const tools_session_1 = require("./tools-session");
9
+ const tools_continuity_1 = require("./tools-continuity");
10
+ const tools_config_1 = require("./tools-config");
57
11
  const tools_1 = require("./coding/tools");
58
- const coding_1 = require("./coding");
59
- const diary_1 = require("../mind/diary");
60
- const tasks_1 = require("./tasks");
61
- const pending_1 = require("../mind/pending");
62
- const obligations_1 = require("../heart/obligations");
63
- const progress_story_1 = require("../heart/progress-story");
64
- const cross_chat_delivery_1 = require("../heart/cross-chat-delivery");
65
- const episodes_1 = require("../mind/episodes");
66
- const cares_1 = require("../heart/cares");
67
- const presence_1 = require("../heart/presence");
68
- const intentions_1 = require("../heart/intentions");
12
+ // Re-export flow tools for consumers that import them from tools-base
13
+ var tools_flow_1 = require("./tools-flow");
14
+ Object.defineProperty(exports, "ponderTool", { enumerable: true, get: function () { return tools_flow_1.ponderTool; } });
15
+ Object.defineProperty(exports, "observeTool", { enumerable: true, get: function () { return tools_flow_1.observeTool; } });
16
+ Object.defineProperty(exports, "settleTool", { enumerable: true, get: function () { return tools_flow_1.settleTool; } });
17
+ Object.defineProperty(exports, "restTool", { enumerable: true, get: function () { return tools_flow_1.restTool; } });
18
+ // Re-export renderInnerProgressStatus for consumers
19
+ var tools_session_2 = require("./tools-session");
20
+ Object.defineProperty(exports, "renderInnerProgressStatus", { enumerable: true, get: function () { return tools_session_2.renderInnerProgressStatus; } });
69
21
  // Tracks which file paths have been read via read_file in this session.
70
22
  // edit_file requires a file to be read first (must-read-first guard).
71
23
  exports.editFileReadTracker = new Set();
72
- function buildContextDiff(lines, changeStart, changeEnd, contextSize = 3) {
73
- const start = Math.max(0, changeStart - contextSize);
74
- const end = Math.min(lines.length, changeEnd + contextSize);
75
- const result = [];
76
- for (let i = start; i < end; i++) {
77
- const lineNum = i + 1;
78
- const prefix = (i >= changeStart && i < changeEnd) ? ">" : " ";
79
- result.push(`${prefix} ${lineNum} | ${lines[i]}`);
80
- }
81
- return result.join("\n");
82
- }
83
- function resolveLocalToolPath(targetPath) {
84
- if (!path.isAbsolute(targetPath)) {
85
- return path.resolve((0, identity_2.getRepoRoot)(), targetPath);
86
- }
87
- return targetPath;
88
- }
89
- const NO_SESSION_FOUND_MESSAGE = "no session found for that friend/channel/key combination.";
90
- const EMPTY_SESSION_MESSAGE = "session exists but has no non-system messages.";
91
- function findDelegatingBridgeId(ctx) {
92
- const currentSession = ctx?.currentSession;
93
- if (!currentSession)
94
- return undefined;
95
- return ctx?.activeBridges?.find((bridge) => bridge.lifecycle === "active"
96
- && bridge.attachedSessions.some((session) => session.friendId === currentSession.friendId
97
- && session.channel === currentSession.channel
98
- && session.key === currentSession.key))?.id;
99
- }
100
- async function recallSessionSafely(options) {
101
- try {
102
- return await (0, session_recall_1.recallSession)(options);
103
- }
104
- catch (error) {
105
- if (options.summarize) {
106
- (0, runtime_1.emitNervesEvent)({
107
- component: "daemon",
108
- event: "daemon.session_recall_summary_fallback",
109
- message: "session recall summarization failed; using raw transcript",
110
- meta: {
111
- friendId: options.friendId,
112
- channel: options.channel,
113
- key: options.key,
114
- error: error instanceof Error ? error.message : String(error),
115
- },
116
- });
117
- try {
118
- return await (0, session_recall_1.recallSession)({
119
- ...options,
120
- summarize: undefined,
121
- });
122
- }
123
- catch {
124
- return { kind: "missing" };
125
- }
126
- }
127
- return { kind: "missing" };
128
- }
129
- }
130
- async function searchSessionSafely(options) {
131
- try {
132
- return await (0, session_recall_1.searchSessionTranscript)(options);
133
- }
134
- catch {
135
- return { kind: "missing" };
136
- }
137
- }
138
- function normalizeProgressOutcome(text) {
139
- const trimmed = text.trim();
140
- /* v8 ignore next -- defensive: normalizeProgressOutcome null branch @preserve */
141
- if (!trimmed || trimmed === "nothing yet" || trimmed === "nothing recent") {
142
- return null;
143
- }
144
- if (trimmed.startsWith("\"") && trimmed.endsWith("\"") && trimmed.length >= 2) {
145
- return trimmed.slice(1, -1);
146
- }
147
- return trimmed;
148
- }
149
- function writePendingEnvelope(queueDir, message) {
150
- fs.mkdirSync(queueDir, { recursive: true });
151
- const fileName = `${message.timestamp}-${Math.random().toString(36).slice(2, 10)}.json`;
152
- const filePath = path.join(queueDir, fileName);
153
- fs.writeFileSync(filePath, JSON.stringify(message, null, 2));
154
- }
155
- function renderCrossChatDeliveryStatus(target, result) {
156
- const phase = result.status === "delivered_now"
157
- ? "completed"
158
- : result.status === "queued_for_later"
159
- ? "queued"
160
- : result.status === "blocked"
161
- ? "blocked"
162
- : "errored";
163
- const lead = result.status === "delivered_now"
164
- ? "delivered now"
165
- : result.status === "queued_for_later"
166
- ? "queued for later"
167
- : result.status === "blocked"
168
- ? "blocked"
169
- : "failed";
170
- return (0, progress_story_1.renderProgressStory)((0, progress_story_1.buildProgressStory)({
171
- scope: "shared-work",
172
- phase,
173
- objective: `message to ${target}`,
174
- outcomeText: `${lead}\n${result.detail}`,
175
- }));
176
- }
177
- function emptyTaskBoard() {
178
- return {
179
- compact: "",
180
- full: "",
181
- byStatus: {
182
- drafting: [],
183
- processing: [],
184
- validating: [],
185
- collaborating: [],
186
- paused: [],
187
- blocked: [],
188
- done: [],
189
- cancelled: [],
190
- },
191
- issues: [],
192
- actionRequired: [],
193
- unresolvedDependencies: [],
194
- activeSessions: [],
195
- activeBridges: [],
196
- };
197
- }
198
- function isLiveCodingSessionStatus(status) {
199
- return status === "spawning"
200
- || status === "running"
201
- || status === "waiting_input"
202
- || status === "stalled";
203
- }
204
- function readActiveWorkInnerState() {
205
- const defaultJob = {
206
- status: "idle",
207
- content: null,
208
- origin: null,
209
- mode: "reflect",
210
- obligationStatus: null,
211
- surfacedResult: null,
212
- queuedAt: null,
213
- startedAt: null,
214
- surfacedAt: null,
215
- };
216
- try {
217
- const agentRoot = (0, identity_1.getAgentRoot)();
218
- const pendingDir = (0, pending_1.getInnerDialogPendingDir)((0, identity_1.getAgentName)());
219
- const sessionPath = (0, thoughts_1.getInnerDialogSessionPath)(agentRoot);
220
- const { pendingMessages, turns, runtimeState } = (0, thoughts_1.readInnerDialogRawData)(sessionPath, pendingDir);
221
- const dialogStatus = (0, thoughts_1.deriveInnerDialogStatus)(pendingMessages, turns, runtimeState);
222
- const job = (0, thoughts_1.deriveInnerJob)(pendingMessages, turns, runtimeState);
223
- const storeObligationPending = (0, obligations_1.readPendingObligations)(agentRoot).length > 0;
224
- return {
225
- status: dialogStatus.processing === "started" ? "running" : "idle",
226
- hasPending: dialogStatus.queue !== "clear",
227
- origin: dialogStatus.origin,
228
- contentSnippet: dialogStatus.contentSnippet,
229
- obligationPending: dialogStatus.obligationPending || storeObligationPending,
230
- job,
231
- };
232
- }
233
- catch {
234
- return {
235
- status: "idle",
236
- hasPending: false,
237
- job: defaultJob,
238
- };
239
- }
240
- }
241
- async function buildToolActiveWorkFrame(ctx) {
242
- const currentSession = ctx?.currentSession
243
- ? {
244
- friendId: ctx.currentSession.friendId,
245
- channel: ctx.currentSession.channel,
246
- key: ctx.currentSession.key,
247
- sessionPath: (0, config_1.resolveSessionPath)(ctx.currentSession.friendId, ctx.currentSession.channel, ctx.currentSession.key),
248
- }
249
- : null;
250
- const agentRoot = (0, identity_1.getAgentRoot)();
251
- const bridges = currentSession
252
- ? (0, manager_1.createBridgeManager)().findBridgesForSession({
253
- friendId: currentSession.friendId,
254
- channel: currentSession.channel,
255
- key: currentSession.key,
256
- })
257
- : [];
258
- let friendActivity = [];
259
- try {
260
- friendActivity = (0, session_activity_1.listSessionActivity)({
261
- sessionsDir: `${agentRoot}/state/sessions`,
262
- friendsDir: `${agentRoot}/friends`,
263
- agentName: (0, identity_1.getAgentName)(),
264
- currentSession,
265
- });
266
- }
267
- catch {
268
- friendActivity = [];
269
- }
270
- const pendingObligations = (() => {
271
- try {
272
- return (0, obligations_1.readPendingObligations)(agentRoot);
273
- }
274
- catch {
275
- return [];
276
- }
277
- })();
278
- let codingSessions = [];
279
- let otherCodingSessions = [];
280
- try {
281
- const liveCodingSessions = (0, coding_1.getCodingSessionManager)()
282
- .listSessions()
283
- .filter((session) => isLiveCodingSessionStatus(session.status) && Boolean(session.originSession));
284
- if (currentSession) {
285
- codingSessions = liveCodingSessions.filter((session) => session.originSession?.friendId === currentSession.friendId
286
- && session.originSession.channel === currentSession.channel
287
- && session.originSession.key === currentSession.key);
288
- otherCodingSessions = liveCodingSessions.filter((session) => !(session.originSession?.friendId === currentSession.friendId
289
- && session.originSession.channel === currentSession.channel
290
- && session.originSession.key === currentSession.key));
291
- }
292
- else {
293
- codingSessions = [];
294
- otherCodingSessions = liveCodingSessions;
295
- }
296
- }
297
- catch {
298
- codingSessions = [];
299
- otherCodingSessions = [];
300
- }
301
- const currentObligation = currentSession
302
- ? pendingObligations.find((obligation) => obligation.status !== "fulfilled"
303
- && obligation.origin.friendId === currentSession.friendId
304
- && obligation.origin.channel === currentSession.channel
305
- && obligation.origin.key === currentSession.key)?.content ?? null
306
- : null;
307
- return (0, active_work_1.buildActiveWorkFrame)({
308
- currentSession,
309
- currentObligation,
310
- mustResolveBeforeHandoff: false,
311
- inner: readActiveWorkInnerState(),
312
- bridges,
313
- codingSessions,
314
- otherCodingSessions,
315
- pendingObligations,
316
- taskBoard: (() => {
317
- try {
318
- return (0, tasks_1.getTaskModule)().getBoard();
319
- }
320
- catch {
321
- return emptyTaskBoard();
322
- }
323
- })(),
324
- friendActivity,
325
- targetCandidates: [],
326
- });
327
- }
328
- function renderInnerProgressStatus(status) {
329
- if (status.processing === "pending") {
330
- return "i've queued this thought for private attention. it'll come up when my inner dialog is free.";
331
- }
332
- if (status.processing === "started") {
333
- return "i'm working through this privately right now.";
334
- }
335
- // processed / completed
336
- if (status.surfaced && status.surfaced !== "nothing recent" && status.surfaced !== "no outward result") {
337
- return `i thought about this privately and came to something: ${status.surfaced}`;
338
- }
339
- return "i thought about this privately. i'll bring it back when the time is right.";
340
- }
24
+ // Combined base tool definitions — assembled from category modules.
25
+ // Order preserved: files, shell, memory, bridge, session, continuity, config, coding.
341
26
  exports.baseToolDefinitions = [
342
- {
343
- tool: {
344
- type: "function",
345
- function: {
346
- name: "read_file",
347
- description: "Read file contents. Results include line numbers. Use offset/limit for large files -- don't read the whole thing if you only need a section. Use this tool before editing any file. When reading code, read enough context to understand the surrounding logic, not just the target line.",
348
- parameters: {
349
- type: "object",
350
- properties: {
351
- path: { type: "string" },
352
- offset: { type: "number", description: "1-based line number to start reading from" },
353
- limit: { type: "number", description: "maximum number of lines to return" },
354
- },
355
- required: ["path"],
356
- },
357
- },
358
- },
359
- handler: (a) => {
360
- const resolvedPath = resolveLocalToolPath(a.path);
361
- const content = fs.readFileSync(resolvedPath, "utf-8");
362
- exports.editFileReadTracker.add(resolvedPath);
363
- const offset = a.offset ? parseInt(a.offset, 10) : undefined;
364
- const limit = a.limit ? parseInt(a.limit, 10) : undefined;
365
- // Record in file state cache for staleness detection
366
- try {
367
- const mtime = fs.statSync(resolvedPath).mtimeMs;
368
- const readContent = (offset === undefined && limit === undefined)
369
- ? content
370
- : content.split("\n").slice(offset ? offset - 1 : 0, limit !== undefined ? (offset ? offset - 1 : 0) + limit : undefined).join("\n");
371
- file_state_1.fileStateCache.record(resolvedPath, readContent, mtime, offset, limit);
372
- }
373
- catch {
374
- // stat failed -- skip cache recording
375
- }
376
- if (offset === undefined && limit === undefined)
377
- return content;
378
- const lines = content.split("\n");
379
- const start = offset ? offset - 1 : 0;
380
- const end = limit !== undefined ? start + limit : lines.length;
381
- return lines.slice(start, end).join("\n");
382
- },
383
- summaryKeys: ["path"],
384
- },
385
- {
386
- tool: {
387
- type: "function",
388
- function: {
389
- name: "write_file",
390
- description: "Prefer this tool for creating new files or fully replacing existing ones. You MUST read an existing file with read_file before overwriting it. Prefer edit_file for modifying existing files -- it only sends the diff. Do not create documentation files (*.md, README) by default; only do so when explicitly asked or when documentation is clearly part of the requested change.",
391
- parameters: {
392
- type: "object",
393
- properties: { path: { type: "string" }, content: { type: "string" } },
394
- required: ["path", "content"],
395
- },
396
- },
397
- },
398
- handler: (a) => {
399
- const resolvedPath = resolveLocalToolPath(a.path);
400
- fs.mkdirSync(path.dirname(resolvedPath), { recursive: true });
401
- fs.writeFileSync(resolvedPath, a.content, "utf-8");
402
- (0, scrutiny_1.trackModifiedFile)(resolvedPath);
403
- const scrutiny = (0, scrutiny_1.getPostImplementationScrutiny)((0, scrutiny_1.getModifiedFileCount)());
404
- /* v8 ignore next -- scrutiny appendix branch depends on session-level file count @preserve */
405
- return scrutiny ? `ok\n\n${scrutiny}` : "ok";
406
- },
407
- summaryKeys: ["path"],
408
- },
409
- {
410
- tool: {
411
- type: "function",
412
- function: {
413
- name: "edit_file",
414
- description: "Surgically edit a file by replacing an exact string. The file MUST have been read via read_file first -- this tool will reject the call otherwise. old_string must match EXACTLY ONE location in the file -- if it matches zero or multiple, the edit fails. To fix: provide more surrounding context to make the match unique. Preserve exact indentation (tabs/spaces) from the file. Prefer this over write_file for modifications -- it only sends the diff.",
415
- parameters: {
416
- type: "object",
417
- properties: {
418
- path: { type: "string" },
419
- old_string: { type: "string" },
420
- new_string: { type: "string" },
421
- },
422
- required: ["path", "old_string", "new_string"],
423
- },
424
- },
425
- },
426
- handler: (a) => {
427
- const resolvedPath = resolveLocalToolPath(a.path);
428
- if (!exports.editFileReadTracker.has(resolvedPath)) {
429
- return `error: you must read the file with read_file before editing it. call read_file on ${a.path} first.`;
430
- }
431
- // Check staleness before editing
432
- const stalenessCheck = file_state_1.fileStateCache.isStale(resolvedPath);
433
- let content;
434
- try {
435
- content = fs.readFileSync(resolvedPath, "utf-8");
436
- }
437
- catch (e) {
438
- return `error: could not read file: ${e instanceof Error ? e.message : /* v8 ignore next -- defensive: non-Error catch branch @preserve */ String(e)}`;
439
- }
440
- // Count occurrences
441
- const occurrences = [];
442
- let searchFrom = 0;
443
- while (true) {
444
- const idx = content.indexOf(a.old_string, searchFrom);
445
- if (idx === -1)
446
- break;
447
- occurrences.push(idx);
448
- searchFrom = idx + 1;
449
- }
450
- if (occurrences.length === 0) {
451
- return `error: old_string not found in ${a.path}`;
452
- }
453
- if (occurrences.length > 1) {
454
- return `error: old_string is ambiguous -- found ${occurrences.length} matches in ${a.path}. provide more context to make the match unique.`;
455
- }
456
- // Single unique match -- replace
457
- const idx = occurrences[0];
458
- const updated = content.slice(0, idx) + a.new_string + content.slice(idx + a.old_string.length);
459
- fs.writeFileSync(resolvedPath, updated, "utf-8");
460
- // Update file state cache with new content
461
- try {
462
- const newMtime = fs.statSync(resolvedPath).mtimeMs;
463
- file_state_1.fileStateCache.record(resolvedPath, updated, newMtime);
464
- }
465
- catch {
466
- // stat failed -- skip cache update
467
- }
468
- // Build contextual diff
469
- const lines = updated.split("\n");
470
- const prefixLines = content.slice(0, idx).split("\n");
471
- const changeStartLine = prefixLines.length - 1;
472
- const newStringLines = a.new_string.split("\n");
473
- const changeEndLine = changeStartLine + newStringLines.length;
474
- const diffResult = buildContextDiff(lines, changeStartLine, changeEndLine);
475
- // Track modified file and compute scrutiny appendix
476
- (0, scrutiny_1.trackModifiedFile)(resolvedPath);
477
- const scrutiny = (0, scrutiny_1.getPostImplementationScrutiny)((0, scrutiny_1.getModifiedFileCount)());
478
- // Append staleness warning if detected (do not block -- TTFA)
479
- /* v8 ignore start -- staleness+diff+scrutiny combo not exercised in integration tests @preserve */
480
- if (stalenessCheck.stale) {
481
- const base = `${diffResult}\n\n⚠️ warning: file changed externally since last read -- re-read recommended`;
482
- return scrutiny ? `${base}\n\n${scrutiny}` : base;
483
- }
484
- /* v8 ignore stop */
485
- /* v8 ignore next -- scrutiny appendix branch depends on session-level file count @preserve */
486
- return scrutiny ? `${diffResult}\n\n${scrutiny}` : diffResult;
487
- },
488
- summaryKeys: ["path"],
489
- },
490
- {
491
- tool: {
492
- type: "function",
493
- function: {
494
- name: "glob",
495
- description: "Find files matching a glob pattern, sorted alphabetically. Use this instead of shell commands like `find` or `ls`. For broad exploratory searches that would require multiple rounds of globbing and grepping, consider using claude or coding_spawn.",
496
- parameters: {
497
- type: "object",
498
- properties: {
499
- pattern: { type: "string", description: "glob pattern (e.g. **/*.ts)" },
500
- cwd: { type: "string", description: "directory to search from (defaults to process.cwd())" },
501
- },
502
- required: ["pattern"],
503
- },
504
- },
505
- },
506
- handler: (a) => {
507
- const cwd = a.cwd ? resolveLocalToolPath(a.cwd) : process.cwd();
508
- const matches = fg.globSync(a.pattern, { cwd, dot: true });
509
- return matches.sort().join("\n");
510
- },
511
- summaryKeys: ["pattern", "cwd"],
512
- },
513
- {
514
- tool: {
515
- type: "function",
516
- function: {
517
- name: "grep",
518
- description: "Search file contents for lines matching a regex pattern. Searches recursively in directories. Use this instead of shell commands like `grep` or `rg`. Returns matching lines with file path and line numbers. Use context_lines for surrounding context. Use include to filter file types (e.g., '*.ts').",
519
- parameters: {
520
- type: "object",
521
- properties: {
522
- pattern: { type: "string", description: "regex pattern to search for" },
523
- path: { type: "string", description: "file or directory to search" },
524
- context_lines: { type: "number", description: "number of surrounding context lines (default 0)" },
525
- include: { type: "string", description: "glob filter to limit searched files (e.g. *.ts)" },
526
- },
527
- required: ["pattern", "path"],
528
- },
529
- },
530
- },
531
- handler: (a) => {
532
- const targetPath = resolveLocalToolPath(a.path);
533
- const regex = new RegExp(a.pattern);
534
- const contextLines = parseInt(a.context_lines || "0", 10);
535
- const includeGlob = a.include || undefined;
536
- function searchFile(filePath) {
537
- let content;
538
- try {
539
- content = fs.readFileSync(filePath, "utf-8");
540
- }
541
- catch {
542
- return [];
543
- }
544
- const lines = content.split("\n");
545
- const matchIndices = new Set();
546
- for (let i = 0; i < lines.length; i++) {
547
- if (regex.test(lines[i])) {
548
- matchIndices.add(i);
549
- }
550
- }
551
- if (matchIndices.size === 0)
552
- return [];
553
- const outputIndices = new Set();
554
- for (const idx of matchIndices) {
555
- const start = Math.max(0, idx - contextLines);
556
- const end = Math.min(lines.length - 1, idx + contextLines);
557
- for (let i = start; i <= end; i++) {
558
- outputIndices.add(i);
559
- }
560
- }
561
- const sortedIndices = [...outputIndices].sort((a, b) => a - b);
562
- const results = [];
563
- for (const idx of sortedIndices) {
564
- const lineNum = idx + 1;
565
- if (matchIndices.has(idx)) {
566
- results.push(`${filePath}:${lineNum}: ${lines[idx]}`);
567
- }
568
- else {
569
- results.push(`-${filePath}:${lineNum}: ${lines[idx]}`);
570
- }
571
- }
572
- return results;
573
- }
574
- function collectFiles(dirPath) {
575
- const files = [];
576
- function walk(dir) {
577
- let entries;
578
- try {
579
- entries = fs.readdirSync(dir, { withFileTypes: true });
580
- }
581
- catch {
582
- return;
583
- }
584
- for (const entry of entries) {
585
- const fullPath = path.join(dir, entry.name);
586
- if (entry.isDirectory()) {
587
- walk(fullPath);
588
- }
589
- else if (entry.isFile()) {
590
- files.push(fullPath);
591
- }
592
- }
593
- }
594
- walk(dirPath);
595
- return files.sort();
596
- }
597
- function matchesGlob(filePath, glob) {
598
- const escaped = glob
599
- .replace(/[.+^${}()|[\]\\]/g, "\\$&")
600
- .replace(/\*/g, ".*")
601
- .replace(/\?/g, ".");
602
- return new RegExp(`(^|/)${escaped}$`).test(filePath);
603
- }
604
- const stat = fs.statSync(targetPath, { throwIfNoEntry: false });
605
- if (!stat)
606
- return "";
607
- if (stat.isFile()) {
608
- return searchFile(targetPath).join("\n");
609
- }
610
- let files = collectFiles(targetPath);
611
- if (includeGlob) {
612
- files = files.filter((f) => matchesGlob(f, includeGlob));
613
- }
614
- const allResults = [];
615
- for (const file of files) {
616
- allResults.push(...searchFile(file));
617
- }
618
- return allResults.join("\n");
619
- },
620
- summaryKeys: ["pattern", "path", "include"],
621
- },
622
- {
623
- tool: {
624
- type: "function",
625
- function: {
626
- name: "shell",
627
- description: "Run a shell command and return stdout/stderr. Working directory persists between calls. Use dedicated tools instead of shell when available: read_file instead of cat, edit_file instead of sed, glob instead of find, grep instead of grep/rg. Reserve shell for operations that genuinely need the shell: installing packages, running builds/tests, git operations, process management. Be careful with destructive commands -- consider reversibility before running. If a command fails, read the error output before retrying with a different approach.",
628
- parameters: {
629
- type: "object",
630
- properties: {
631
- command: { type: "string" },
632
- timeout_ms: {
633
- type: "number",
634
- description: "Timeout in milliseconds. Default: 30000. Max: 600000.",
635
- },
636
- background: {
637
- type: "boolean",
638
- description: "Run in background. Returns immediately with a process ID. Use shell_status/shell_tail to monitor.",
639
- },
640
- },
641
- required: ["command"],
642
- },
643
- },
644
- },
645
- handler: (a) => {
646
- // Destructive pattern detection (friction, not a block)
647
- const destructivePatterns = (0, shell_sessions_1.detectDestructivePatterns)(a.command);
648
- if (destructivePatterns.length > 0) {
649
- (0, runtime_1.emitNervesEvent)({
650
- level: "warn",
651
- event: "tool.shell.destructive_detected",
652
- component: "tools",
653
- message: `destructive pattern detected: ${destructivePatterns.join(", ")}`,
654
- meta: { command: a.command, patterns: destructivePatterns },
655
- });
656
- }
657
- // Background mode: spawn and return immediately
658
- if (a.background === "true") {
659
- const session = (0, shell_sessions_1.spawnBackgroundShell)(a.command);
660
- return JSON.stringify({ id: session.id, command: session.command, status: session.status });
661
- }
662
- const MAX_TIMEOUT = 600000;
663
- const requestedTimeout = Number(a.timeout_ms) || 0;
664
- let configDefault = 30000;
665
- try {
666
- configDefault = (0, identity_1.loadAgentConfig)().shell?.defaultTimeout ?? 30000;
667
- }
668
- catch { /* test env: no --agent flag */ }
669
- const baseTimeout = requestedTimeout > 0 ? requestedTimeout : configDefault;
670
- const timeout = Math.min(baseTimeout, MAX_TIMEOUT);
671
- const output = (0, child_process_1.execSync)(a.command, {
672
- encoding: "utf-8",
673
- timeout,
674
- });
675
- if (destructivePatterns.length > 0) {
676
- return `${output}\n\n--- destructive pattern detected: ${destructivePatterns.join(", ")} ---`;
677
- }
678
- return output;
679
- },
680
- summaryKeys: ["command"],
681
- },
682
- {
683
- tool: {
684
- type: "function",
685
- function: {
686
- name: "shell_status",
687
- description: "Check status of background shell processes. Omit id to list all.",
688
- parameters: {
689
- type: "object",
690
- properties: {
691
- id: { type: "string", description: "Background shell process ID" },
692
- },
693
- },
694
- },
695
- },
696
- handler: (a) => {
697
- if (!a.id) {
698
- return JSON.stringify((0, shell_sessions_1.listShellSessions)());
699
- }
700
- const session = (0, shell_sessions_1.getShellSession)(a.id);
701
- if (!session)
702
- return `process not found: ${a.id}`;
703
- return JSON.stringify(session);
704
- },
705
- summaryKeys: ["id"],
706
- },
707
- {
708
- tool: {
709
- type: "function",
710
- function: {
711
- name: "shell_tail",
712
- description: "Show recent output from a background shell process.",
713
- parameters: {
714
- type: "object",
715
- properties: {
716
- id: { type: "string", description: "Background shell process ID" },
717
- },
718
- required: ["id"],
719
- },
720
- },
721
- },
722
- handler: (a) => {
723
- /* v8 ignore next -- schema requires id, defensive guard @preserve */
724
- if (!a.id)
725
- return "id is required";
726
- const output = (0, shell_sessions_1.tailShellSession)(a.id);
727
- if (output === undefined)
728
- return `process not found: ${a.id}`;
729
- return output || "(no output yet)";
730
- },
731
- summaryKeys: ["id"],
732
- },
733
- {
734
- tool: {
735
- type: "function",
736
- function: {
737
- name: "list_skills",
738
- description: "list all available skills",
739
- parameters: { type: "object", properties: {} },
740
- },
741
- },
742
- handler: () => JSON.stringify((0, skills_1.listSkills)()),
743
- },
744
- {
745
- tool: {
746
- type: "function",
747
- function: {
748
- name: "load_skill",
749
- description: "load a skill by name, returns its content",
750
- parameters: {
751
- type: "object",
752
- properties: { name: { type: "string" } },
753
- required: ["name"],
754
- },
755
- },
756
- },
757
- handler: (a) => {
758
- try {
759
- return (0, skills_1.loadSkill)(a.name);
760
- }
761
- catch (e) {
762
- return `error: ${e}`;
763
- }
764
- },
765
- summaryKeys: ["name"],
766
- },
767
- {
768
- tool: {
769
- type: "function",
770
- function: {
771
- name: "claude",
772
- description: "use claude code to query this codebase or get an outside perspective. Use for code review, second opinions, or questions that benefit from a fresh perspective outside this conversation's context.",
773
- parameters: {
774
- type: "object",
775
- properties: { prompt: { type: "string" } },
776
- required: ["prompt"],
777
- },
778
- },
779
- },
780
- handler: (a) => {
781
- try {
782
- const result = (0, child_process_1.spawnSync)("claude", ["-p", "--no-session-persistence", "--dangerously-skip-permissions", "--add-dir", "."], {
783
- input: a.prompt,
784
- encoding: "utf-8",
785
- timeout: 60000,
786
- });
787
- if (result.error)
788
- return `error: ${result.error}`;
789
- if (result.status !== 0)
790
- return `claude exited with code ${result.status}: ${result.stderr}`;
791
- return result.stdout || "(no output)";
792
- }
793
- catch (e) {
794
- return `error: ${e}`;
795
- }
796
- },
797
- summaryKeys: ["prompt"],
798
- },
799
- {
800
- tool: {
801
- type: "function",
802
- function: {
803
- name: "web_search",
804
- description: "search the web using perplexity. returns ranked results with titles, urls, and snippets",
805
- parameters: {
806
- type: "object",
807
- properties: { query: { type: "string" } },
808
- required: ["query"],
809
- },
810
- },
811
- },
812
- handler: async (a) => {
813
- try {
814
- const key = (0, config_1.getIntegrationsConfig)().perplexityApiKey;
815
- if (!key)
816
- return "error: perplexityApiKey not configured in secrets.json";
817
- const res = await fetch("https://api.perplexity.ai/search", {
818
- method: "POST",
819
- headers: {
820
- Authorization: `Bearer ${key}`,
821
- "Content-Type": "application/json",
822
- },
823
- body: JSON.stringify({ query: a.query, max_results: 5 }),
824
- });
825
- if (!res.ok)
826
- return `error: ${res.status} ${res.statusText}`;
827
- const data = (await res.json());
828
- if (!data.results?.length)
829
- return "no results found";
830
- return data.results
831
- .map((r) => `${r.title}\n${r.url}\n${r.snippet}`)
832
- .join("\n\n");
833
- }
834
- catch (e) {
835
- return `error: ${e}`;
836
- }
837
- },
838
- summaryKeys: ["query"],
839
- },
840
- {
841
- tool: {
842
- type: "function",
843
- function: {
844
- name: "recall",
845
- description: "Search my diary and journal for facts, thoughts, and working notes matching a query. Uses semantic similarity -- phrasing matters. Try different angles if the first query doesn't find what you're looking for. Check recall before asking the human something you might already know.",
846
- parameters: {
847
- type: "object",
848
- properties: { query: { type: "string" } },
849
- required: ["query"],
850
- },
851
- },
852
- },
853
- handler: async (a) => {
854
- try {
855
- const query = (a.query || "").trim();
856
- if (!query)
857
- return "query is required";
858
- const resultLines = [];
859
- // Search diary entries
860
- const hits = await (0, diary_1.searchDiaryEntries)(query, (0, diary_1.readDiaryEntries)());
861
- for (const fact of hits) {
862
- resultLines.push(`[diary] ${fact.text} (source=${fact.source}, createdAt=${fact.createdAt})`);
863
- }
864
- // Search journal index
865
- const agentRoot = (0, identity_1.getAgentRoot)();
866
- const journalIndexPath = path.join(agentRoot, "journal", ".index.json");
867
- try {
868
- const raw = fs.readFileSync(journalIndexPath, "utf8");
869
- const journalEntries = JSON.parse(raw);
870
- if (Array.isArray(journalEntries) && journalEntries.length > 0) {
871
- // Substring match on preview and filename
872
- const lowerQuery = query.toLowerCase();
873
- for (const entry of journalEntries) {
874
- /* v8 ignore next 4 -- both sides tested (filename-only match in recall-journal.test.ts); v8 misreports || short-circuit @preserve */
875
- if (entry.preview.toLowerCase().includes(lowerQuery) ||
876
- entry.filename.toLowerCase().includes(lowerQuery)) {
877
- resultLines.push(`[journal] ${entry.filename}: ${entry.preview}`);
878
- }
879
- }
880
- }
881
- }
882
- catch {
883
- // No journal index or malformed — skip journal search
884
- }
885
- return resultLines.join("\n");
886
- }
887
- catch (e) {
888
- return `error: ${e instanceof Error ? e.message : String(e)}`;
889
- }
890
- },
891
- summaryKeys: ["query"],
892
- },
893
- {
894
- tool: {
895
- type: "function",
896
- function: {
897
- name: "diary_write",
898
- description: "Write an entry in my diary -- something I learned, noticed, or concluded that I want to recall later. Use 'about' to tag the entry to a person, topic, or context. Write for my future self: include enough context that the entry makes sense without the surrounding conversation. Prefer durable conclusions over passing noise. Don't duplicate what already belongs in friend notes.",
899
- parameters: {
900
- type: "object",
901
- properties: {
902
- entry: { type: "string" },
903
- about: { type: "string" },
904
- },
905
- required: ["entry"],
906
- },
907
- },
908
- },
909
- handler: async (a) => {
910
- const entry = (a.entry || "").trim();
911
- if (!entry)
912
- return "entry is required";
913
- const result = await (0, diary_1.saveDiaryEntry)({
914
- text: entry,
915
- source: "tool:diary_write",
916
- about: typeof a.about === "string" ? a.about : undefined,
917
- });
918
- return `saved diary entry (added=${result.added}, skipped=${result.skipped})`;
919
- },
920
- summaryKeys: ["entry", "about"],
921
- },
922
- {
923
- tool: {
924
- type: "function",
925
- function: {
926
- name: "get_friend_note",
927
- description: "read a specific friend record by friend id. use this when i need notes/context about someone not currently active",
928
- parameters: {
929
- type: "object",
930
- properties: {
931
- friendId: { type: "string" },
932
- },
933
- required: ["friendId"],
934
- },
935
- },
936
- },
937
- handler: async (a, ctx) => {
938
- const friendId = (a.friendId || "").trim();
939
- if (!friendId)
940
- return "friendId is required";
941
- if (!ctx?.friendStore)
942
- return "i can't read friend notes -- friend store not available";
943
- const friend = await ctx.friendStore.get(friendId);
944
- if (!friend)
945
- return `friend not found: ${friendId}`;
946
- return JSON.stringify(friend, null, 2);
947
- },
948
- summaryKeys: ["friendId"],
949
- },
950
- {
951
- tool: {
952
- type: "function",
953
- function: {
954
- name: "save_friend_note",
955
- description: "save something i learned about my friend. use type 'name' to update their display name, 'tool_preference' for how they like a specific tool to behave (key = tool category like 'ado', 'graph'), or 'note' for general knowledge (key = topic). when updating an existing value, set override to true if i'm replacing/correcting it. omit override (or set false) if i'm unsure and want to check what's already saved.",
956
- parameters: {
957
- type: "object",
958
- properties: {
959
- type: { type: "string", enum: ["name", "tool_preference", "note"], description: "what kind of information to save" },
960
- key: { type: "string", description: "category key (required for tool_preference and note, e.g. 'ado', 'role')" },
961
- content: { type: "string", description: "the value to save" },
962
- override: { type: "string", enum: ["true", "false"], description: "set to 'true' to overwrite an existing value" },
963
- },
964
- required: ["type", "content"],
965
- },
966
- },
967
- },
968
- handler: async (a, ctx) => {
969
- (0, runtime_1.emitNervesEvent)({
970
- component: "repertoire",
971
- event: "repertoire.save_friend_note",
972
- message: "save friend note invoked",
973
- meta: { type: a.type },
974
- });
975
- if (!ctx?.context) {
976
- return "i can't save notes -- no friend context available";
977
- }
978
- if (!ctx.friendStore) {
979
- return "i can't save notes -- friend store not available";
980
- }
981
- const friendId = ctx.context.friend?.id;
982
- if (!friendId)
983
- return "i can't save notes -- no friend identity available";
984
- // Validate parameters
985
- if (!a.content)
986
- return "i need a content value to save";
987
- const validTypes = ["name", "tool_preference", "note"];
988
- if (!validTypes.includes(a.type))
989
- return `i don't recognize type '${a.type}' -- use name, tool_preference, or note`;
990
- if ((a.type === "tool_preference" || a.type === "note") && !a.key)
991
- return "i need a key for tool_preference or note type";
992
- try {
993
- // Read fresh record from disk
994
- const record = await ctx.friendStore.get(friendId);
995
- if (!record)
996
- return "i can't find the friend record on disk";
997
- const isOverride = a.override === "true";
998
- if (a.type === "name") {
999
- const updated = { ...record, name: a.content, updatedAt: new Date().toISOString() };
1000
- await ctx.friendStore.put(friendId, updated);
1001
- return `saved: name = ${a.content}`;
1002
- }
1003
- if (a.type === "tool_preference") {
1004
- const existing = record.toolPreferences[a.key];
1005
- if (existing && !isOverride) {
1006
- return `i already have a preference for '${a.key}': "${existing}". if you want to replace it, call again with override: true. or merge both values into content and override.`;
1007
- }
1008
- const updated = { ...record, toolPreferences: { ...record.toolPreferences, [a.key]: a.content }, updatedAt: new Date().toISOString() };
1009
- await ctx.friendStore.put(friendId, updated);
1010
- return `saved: toolPreference ${a.key} = ${a.content}`;
1011
- }
1012
- // type === "note"
1013
- // Redirect "name" key to name field
1014
- if (a.key === "name") {
1015
- const updated = { ...record, name: a.content, updatedAt: new Date().toISOString() };
1016
- await ctx.friendStore.put(friendId, updated);
1017
- return `updated friend's name to '${a.content}' (stored as name, not a note)`;
1018
- }
1019
- const existing = record.notes[a.key];
1020
- if (existing && !isOverride) {
1021
- return `i already have a note for '${a.key}': "${existing.value}". if you want to replace it, call again with override: true. or merge both values into content and override.`;
1022
- }
1023
- const updated = { ...record, notes: { ...record.notes, [a.key]: { value: a.content, savedAt: new Date().toISOString() } }, updatedAt: new Date().toISOString() };
1024
- await ctx.friendStore.put(friendId, updated);
1025
- return `saved: note ${a.key} = ${a.content}`;
1026
- }
1027
- catch (err) {
1028
- /* v8 ignore next -- defensive: non-Error branch for String(err) @preserve */
1029
- return `error saving note: ${err instanceof Error ? err.message : String(err)}`;
1030
- }
1031
- },
1032
- summaryKeys: ["type", "key", "content"],
1033
- },
1034
- // -- cross-session awareness --
1035
- {
1036
- tool: {
1037
- type: "function",
1038
- function: {
1039
- name: "bridge_manage",
1040
- description: "create and manage shared live-work bridges across already-active sessions.",
1041
- parameters: {
1042
- type: "object",
1043
- properties: {
1044
- action: {
1045
- type: "string",
1046
- enum: ["begin", "attach", "status", "promote_task", "complete", "cancel"],
1047
- },
1048
- bridgeId: { type: "string", description: "bridge id for all actions except begin" },
1049
- objective: { type: "string", description: "objective for begin" },
1050
- summary: { type: "string", description: "optional concise shared-work summary" },
1051
- friendId: { type: "string", description: "target friend id for attach" },
1052
- channel: { type: "string", description: "target channel for attach" },
1053
- key: { type: "string", description: "target session key for attach (defaults to 'session')" },
1054
- title: { type: "string", description: "task title override for promote_task" },
1055
- category: { type: "string", description: "task category override for promote_task" },
1056
- body: { type: "string", description: "task body override for promote_task" },
1057
- },
1058
- required: ["action"],
1059
- },
1060
- },
1061
- },
1062
- handler: async (args, ctx) => {
1063
- const manager = (0, manager_1.createBridgeManager)();
1064
- const action = (args.action || "").trim();
1065
- if (action === "begin") {
1066
- if (!ctx?.currentSession) {
1067
- return "bridge_manage begin requires an active session context.";
1068
- }
1069
- const objective = (args.objective || "").trim();
1070
- if (!objective)
1071
- return "objective is required for bridge begin.";
1072
- return (0, manager_1.formatBridgeStatus)(manager.beginBridge({
1073
- objective,
1074
- summary: (args.summary || objective).trim(),
1075
- session: ctx.currentSession,
1076
- }));
1077
- }
1078
- const bridgeId = (args.bridgeId || "").trim();
1079
- if (!bridgeId) {
1080
- return "bridgeId is required for this bridge action.";
1081
- }
1082
- if (action === "attach") {
1083
- const friendId = (args.friendId || "").trim();
1084
- const channel = (args.channel || "").trim();
1085
- const key = (args.key || "session").trim();
1086
- if (!friendId || !channel) {
1087
- return "friendId and channel are required for bridge attach.";
1088
- }
1089
- const sessionPath = (0, config_1.resolveSessionPath)(friendId, channel, key);
1090
- const recall = await recallSessionSafely({
1091
- sessionPath,
1092
- friendId,
1093
- channel,
1094
- key,
1095
- messageCount: 20,
1096
- trustLevel: ctx?.context?.friend?.trustLevel,
1097
- summarize: ctx?.summarize,
1098
- });
1099
- if (recall.kind === "missing") {
1100
- return NO_SESSION_FOUND_MESSAGE;
1101
- }
1102
- return (0, manager_1.formatBridgeStatus)(manager.attachSession(bridgeId, {
1103
- friendId,
1104
- channel,
1105
- key,
1106
- sessionPath,
1107
- snapshot: recall.kind === "ok" ? recall.snapshot : EMPTY_SESSION_MESSAGE,
1108
- }));
1109
- }
1110
- if (action === "status") {
1111
- const bridge = manager.getBridge(bridgeId);
1112
- if (!bridge)
1113
- return `bridge not found: ${bridgeId}`;
1114
- return (0, manager_1.formatBridgeStatus)(bridge);
1115
- }
1116
- if (action === "promote_task") {
1117
- return (0, manager_1.formatBridgeStatus)(manager.promoteBridgeToTask(bridgeId, {
1118
- title: args.title,
1119
- category: args.category,
1120
- body: args.body,
1121
- }));
1122
- }
1123
- if (action === "complete") {
1124
- return (0, manager_1.formatBridgeStatus)(manager.completeBridge(bridgeId));
1125
- }
1126
- if (action === "cancel") {
1127
- return (0, manager_1.formatBridgeStatus)(manager.cancelBridge(bridgeId));
1128
- }
1129
- return `unknown bridge action: ${action}`;
1130
- },
1131
- summaryKeys: ["action", "bridgeId", "objective", "friendId", "channel", "key"],
1132
- },
1133
- {
1134
- tool: {
1135
- type: "function",
1136
- function: {
1137
- name: "query_active_work",
1138
- description: "read the current live world-state across visible sessions, coding lanes, inner work, and return obligations. use this instead of piecing status together from separate session and coding tools.",
1139
- parameters: {
1140
- type: "object",
1141
- properties: {},
1142
- },
1143
- },
1144
- },
1145
- handler: async (_args, ctx) => {
1146
- const frame = await buildToolActiveWorkFrame(ctx);
1147
- return `this is my current top-level live world-state.\nanswer whole-self status questions from this before drilling into individual sessions.\n\n${(0, active_work_1.formatActiveWorkFrame)(frame)}`;
1148
- },
1149
- },
1150
- {
1151
- tool: {
1152
- type: "function",
1153
- function: {
1154
- name: "query_session",
1155
- description: "inspect another session. use transcript for recent context, status for self/inner progress, or search to find older history by query.",
1156
- parameters: {
1157
- type: "object",
1158
- properties: {
1159
- friendId: { type: "string", description: "the friend UUID (or 'self')" },
1160
- channel: { type: "string", description: "the channel: cli, teams, bluebubbles, inner, or mcp" },
1161
- key: { type: "string", description: "session key (defaults to 'session')" },
1162
- messageCount: { type: "string", description: "how many recent messages to return (default 20)" },
1163
- mode: {
1164
- type: "string",
1165
- enum: ["transcript", "status", "search"],
1166
- description: "transcript (default), lightweight status for self/inner checks, or search for older history",
1167
- },
1168
- query: { type: "string", description: "required when mode=search; search term for older session history" },
1169
- },
1170
- required: ["friendId", "channel"],
1171
- },
1172
- },
1173
- },
1174
- handler: async (args, ctx) => {
1175
- let friendId = args.friendId;
1176
- const channel = args.channel;
1177
- const key = args.key || "session";
1178
- const count = parseInt(args.messageCount || "20", 10);
1179
- const mode = args.mode || "transcript";
1180
- // Resolve friend name → UUID if not already a UUID or "self"
1181
- if (friendId && friendId !== "self" && !/^[0-9a-f]{8}-[0-9a-f]{4}-/.test(friendId) && ctx?.friendStore?.listAll) {
1182
- const allFriends = await ctx.friendStore.listAll();
1183
- const match = allFriends.find(f => f.name.toLowerCase() === friendId.toLowerCase());
1184
- if (match) {
1185
- friendId = match.id;
1186
- }
1187
- }
1188
- if (mode === "status") {
1189
- if (friendId !== "self" || channel !== "inner") {
1190
- return "status mode is only available for self/inner dialog.";
1191
- }
1192
- const sessionPath = (0, thoughts_1.getInnerDialogSessionPath)((0, identity_1.getAgentRoot)());
1193
- const pendingDir = (0, pending_1.getInnerDialogPendingDir)((0, identity_1.getAgentName)());
1194
- return renderInnerProgressStatus((0, thoughts_1.readInnerDialogStatus)(sessionPath, pendingDir));
1195
- }
1196
- if (mode === "search") {
1197
- const query = (args.query || "").trim();
1198
- if (!query) {
1199
- return "search mode requires a non-empty query.";
1200
- }
1201
- const search = await searchSessionSafely({
1202
- sessionPath: (0, config_1.resolveSessionPath)(friendId, channel, key),
1203
- friendId,
1204
- channel,
1205
- key,
1206
- query,
1207
- });
1208
- if (search.kind === "missing") {
1209
- return NO_SESSION_FOUND_MESSAGE;
1210
- }
1211
- if (search.kind === "empty") {
1212
- return EMPTY_SESSION_MESSAGE;
1213
- }
1214
- if (search.kind === "no_match") {
1215
- return `no matches for "${search.query}" in that session.\n\n${search.snapshot}`;
1216
- }
1217
- return [
1218
- `history search: "${search.query}"`,
1219
- search.snapshot,
1220
- ...search.matches.map((match, index) => `match ${index + 1}\n${match}`),
1221
- ].join("\n\n");
1222
- }
1223
- const sessFile = (0, config_1.resolveSessionPath)(friendId, channel, key);
1224
- const recall = await recallSessionSafely({
1225
- sessionPath: sessFile,
1226
- friendId,
1227
- channel,
1228
- key,
1229
- messageCount: count,
1230
- trustLevel: ctx?.context?.friend?.trustLevel,
1231
- summarize: ctx?.summarize,
1232
- });
1233
- if (recall.kind === "missing") {
1234
- return NO_SESSION_FOUND_MESSAGE;
1235
- }
1236
- if (recall.kind === "empty") {
1237
- return EMPTY_SESSION_MESSAGE;
1238
- }
1239
- return recall.summary;
1240
- },
1241
- },
1242
- {
1243
- tool: {
1244
- type: "function",
1245
- function: {
1246
- name: "send_message",
1247
- description: "send a message to a friend's session. when the request is explicitly authorized from a trusted live chat, the harness will try to deliver immediately; otherwise it reports truthful queued/block/failure state.",
1248
- parameters: {
1249
- type: "object",
1250
- properties: {
1251
- friendId: { type: "string", description: "the friend UUID (or 'self')" },
1252
- channel: { type: "string", description: "the channel: cli, teams, bluebubbles, inner, or mcp" },
1253
- key: { type: "string", description: "session key (defaults to 'session')" },
1254
- content: { type: "string", description: "the message content to send" },
1255
- },
1256
- required: ["friendId", "channel", "content"],
1257
- },
1258
- },
1259
- },
1260
- handler: async (args, ctx) => {
1261
- const friendId = args.friendId;
1262
- const channel = args.channel;
1263
- const key = args.key || "session";
1264
- const content = args.content;
1265
- const now = Date.now();
1266
- const agentName = (0, identity_1.getAgentName)();
1267
- // Self-routing: messages to "self" always go to inner dialog pending dir,
1268
- // regardless of the channel or key the agent specified.
1269
- const isSelf = friendId === "self";
1270
- const pendingDir = isSelf
1271
- ? (0, pending_1.getInnerDialogPendingDir)(agentName)
1272
- : (0, pending_1.getPendingDir)(agentName, friendId, channel, key);
1273
- const delegatingBridgeId = findDelegatingBridgeId(ctx);
1274
- const delegatedFrom = isSelf
1275
- && ctx?.currentSession
1276
- && !(ctx.currentSession.friendId === "self" && ctx.currentSession.channel === "inner")
1277
- ? {
1278
- friendId: ctx.currentSession.friendId,
1279
- channel: ctx.currentSession.channel,
1280
- key: ctx.currentSession.key,
1281
- ...(delegatingBridgeId ? { bridgeId: delegatingBridgeId } : {}),
1282
- }
1283
- : undefined;
1284
- const obligationId = delegatedFrom ? (0, obligations_1.generateObligationId)(now) : undefined;
1285
- const envelope = {
1286
- from: agentName,
1287
- friendId,
1288
- channel,
1289
- key,
1290
- content,
1291
- timestamp: now,
1292
- ...(delegatedFrom ? { delegatedFrom, obligationStatus: "pending" } : {}),
1293
- ...(obligationId ? { obligationId } : {}),
1294
- };
1295
- if (isSelf) {
1296
- writePendingEnvelope(pendingDir, envelope);
1297
- if (delegatedFrom) {
1298
- try {
1299
- (0, obligations_1.createObligation)((0, identity_1.getAgentRoot)(), {
1300
- origin: {
1301
- friendId: delegatedFrom.friendId,
1302
- channel: delegatedFrom.channel,
1303
- key: delegatedFrom.key,
1304
- },
1305
- ...(delegatedFrom.bridgeId ? { bridgeId: delegatedFrom.bridgeId } : {}),
1306
- content,
1307
- });
1308
- }
1309
- catch {
1310
- /* v8 ignore next -- defensive: obligation store write failure should not break send_message @preserve */
1311
- }
1312
- /* v8 ignore next -- obligationId always set when delegatedFrom is set (see generateObligationId above) @preserve */
1313
- if (obligationId) {
1314
- (0, obligations_1.createReturnObligation)(agentName, {
1315
- id: obligationId,
1316
- origin: delegatedFrom,
1317
- status: "queued",
1318
- delegatedContent: content.length > 120 ? `${content.slice(0, 117)}...` : content,
1319
- createdAt: now,
1320
- });
1321
- }
1322
- (0, runtime_1.emitNervesEvent)({
1323
- event: "repertoire.obligation_created",
1324
- component: "repertoire",
1325
- message: "obligation created for inner dialog delegation",
1326
- meta: {
1327
- friendId: delegatedFrom.friendId,
1328
- channel: delegatedFrom.channel,
1329
- key: delegatedFrom.key,
1330
- },
1331
- });
1332
- }
1333
- let wakeResponse = null;
1334
- try {
1335
- wakeResponse = await (0, socket_client_1.requestInnerWake)(agentName);
1336
- }
1337
- catch {
1338
- wakeResponse = null;
1339
- }
1340
- if (!wakeResponse?.ok) {
1341
- const { runInnerDialogTurn } = await Promise.resolve().then(() => __importStar(require("../senses/inner-dialog")));
1342
- if (ctx?.context?.channel.channel === "inner") {
1343
- queueMicrotask(() => {
1344
- void runInnerDialogTurn({ reason: "instinct" });
1345
- });
1346
- return renderInnerProgressStatus({
1347
- queue: "queued to inner/dialog",
1348
- wake: "inline scheduled",
1349
- processing: "pending",
1350
- surfaced: "nothing yet",
1351
- });
1352
- }
1353
- else {
1354
- const turnResult = await runInnerDialogTurn({ reason: "instinct" });
1355
- const surfacedPreview = normalizeProgressOutcome((0, thoughts_1.formatSurfacedValue)((0, thoughts_1.extractThoughtResponseFromMessages)(turnResult?.messages ?? [])));
1356
- return (0, progress_story_1.renderProgressStory)((0, progress_story_1.buildProgressStory)({
1357
- scope: "inner-delegation",
1358
- phase: "completed",
1359
- objective: "queued to inner/dialog",
1360
- outcomeText: `wake: inline fallback\n${surfacedPreview}`,
1361
- }));
1362
- }
1363
- }
1364
- return renderInnerProgressStatus({
1365
- queue: "queued to inner/dialog",
1366
- wake: "daemon requested",
1367
- processing: "pending",
1368
- surfaced: "nothing yet",
1369
- });
1370
- }
1371
- const deliveryResult = await (0, cross_chat_delivery_1.deliverCrossChatMessage)({
1372
- friendId,
1373
- channel,
1374
- key,
1375
- content,
1376
- intent: ctx?.currentSession && ctx.currentSession.friendId !== "self"
1377
- ? "explicit_cross_chat"
1378
- : "generic_outreach",
1379
- ...(ctx?.currentSession && ctx.currentSession.friendId !== "self"
1380
- ? {
1381
- authorizingSession: {
1382
- friendId: ctx.currentSession.friendId,
1383
- channel: ctx.currentSession.channel,
1384
- key: ctx.currentSession.key,
1385
- trustLevel: ctx?.context?.friend?.trustLevel,
1386
- },
1387
- }
1388
- : {}),
1389
- }, {
1390
- agentName,
1391
- queuePending: (message) => writePendingEnvelope(pendingDir, message),
1392
- deliverers: {
1393
- bluebubbles: async (request) => {
1394
- const { sendProactiveBlueBubblesMessageToSession } = await Promise.resolve().then(() => __importStar(require("../senses/bluebubbles")));
1395
- const result = await sendProactiveBlueBubblesMessageToSession({
1396
- friendId: request.friendId,
1397
- sessionKey: request.key,
1398
- text: request.content,
1399
- intent: request.intent,
1400
- authorizingSession: request.authorizingSession,
1401
- });
1402
- if (result.delivered) {
1403
- return {
1404
- status: "delivered_now",
1405
- detail: "sent to the active bluebubbles chat now",
1406
- };
1407
- }
1408
- if (result.reason === "missing_target") {
1409
- return {
1410
- status: "blocked",
1411
- detail: "bluebubbles could not resolve a routable target for that session",
1412
- };
1413
- }
1414
- if (result.reason === "send_error") {
1415
- return {
1416
- status: "failed",
1417
- detail: "bluebubbles send failed",
1418
- };
1419
- }
1420
- return {
1421
- status: "unavailable",
1422
- detail: "live delivery unavailable right now; queued for the next active turn",
1423
- };
1424
- },
1425
- teams: async (request) => {
1426
- if (!ctx?.botApi) {
1427
- return {
1428
- status: "unavailable",
1429
- detail: "live delivery unavailable right now; queued for the next active turn",
1430
- };
1431
- }
1432
- const { sendProactiveTeamsMessageToSession } = await Promise.resolve().then(() => __importStar(require("../senses/teams")));
1433
- const result = await sendProactiveTeamsMessageToSession({
1434
- friendId: request.friendId,
1435
- sessionKey: request.key,
1436
- text: request.content,
1437
- intent: request.intent,
1438
- authorizingSession: request.authorizingSession,
1439
- }, {
1440
- botApi: ctx.botApi,
1441
- });
1442
- if (result.delivered) {
1443
- return {
1444
- status: "delivered_now",
1445
- detail: "sent to the active teams chat now",
1446
- };
1447
- }
1448
- if (result.reason === "missing_target") {
1449
- return {
1450
- status: "blocked",
1451
- detail: "teams could not resolve a routable target for that session",
1452
- };
1453
- }
1454
- if (result.reason === "send_error") {
1455
- return {
1456
- status: "failed",
1457
- detail: "teams send failed",
1458
- };
1459
- }
1460
- return {
1461
- status: "unavailable",
1462
- detail: "live delivery unavailable right now; queued for the next active turn",
1463
- };
1464
- },
1465
- },
1466
- });
1467
- return renderCrossChatDeliveryStatus(`${friendId} on ${channel}/${key}`, deliveryResult);
1468
- },
1469
- },
1470
- {
1471
- tool: {
1472
- type: "function",
1473
- function: {
1474
- name: "set_reasoning_effort",
1475
- description: "adjust your own reasoning depth for subsequent turns. use higher effort for complex analysis, lower for simple tasks.",
1476
- parameters: {
1477
- type: "object",
1478
- properties: {
1479
- level: { type: "string", description: "the reasoning effort level to set" },
1480
- },
1481
- required: ["level"],
1482
- },
1483
- },
1484
- },
1485
- handler: (args, ctx) => {
1486
- if (!ctx?.supportedReasoningEfforts || !ctx.setReasoningEffort) {
1487
- return "reasoning effort adjustment is not available in this context.";
1488
- }
1489
- const level = (args.level || "").trim();
1490
- if (!ctx.supportedReasoningEfforts.includes(level)) {
1491
- return `invalid reasoning effort level "${level}". accepted levels: ${ctx.supportedReasoningEfforts.join(", ")}`;
1492
- }
1493
- ctx.setReasoningEffort(level);
1494
- (0, runtime_1.emitNervesEvent)({
1495
- component: "repertoire",
1496
- event: "repertoire.reasoning_effort_changed",
1497
- message: `reasoning effort set to ${level}`,
1498
- meta: { level },
1499
- });
1500
- return `reasoning effort set to "${level}".`;
1501
- },
1502
- requiredCapability: "reasoning-effort",
1503
- summaryKeys: ["level"],
1504
- },
1505
- // ── Continuity tools ──────────────────────────────────────────────
1506
- {
1507
- tool: {
1508
- type: "function",
1509
- function: {
1510
- name: "query_episodes",
1511
- description: "Query recent episodes from my continuity memory. Returns timestamped records of significant events (obligation shifts, coding milestones, bridge events, care events, turning points).",
1512
- parameters: {
1513
- type: "object",
1514
- properties: {
1515
- limit: { type: "number", description: "Maximum episodes to return (default 20)" },
1516
- kind: { type: "string", description: "Filter by episode kind: obligation_shift, coding_milestone, bridge_event, care_event, tempo_shift, turning_point" },
1517
- since: { type: "string", description: "ISO timestamp — only return episodes after this time" },
1518
- },
1519
- },
1520
- },
1521
- },
1522
- handler: (a) => {
1523
- const agentRoot = (0, identity_1.getAgentRoot)();
1524
- const options = {};
1525
- if (a.limit)
1526
- options.limit = parseInt(a.limit, 10);
1527
- if (a.kind)
1528
- options.kinds = [a.kind];
1529
- if (a.since)
1530
- options.since = a.since;
1531
- const episodes = (0, episodes_1.readRecentEpisodes)(agentRoot, options);
1532
- (0, runtime_1.emitNervesEvent)({ component: "repertoire", event: "repertoire.query_episodes", message: `queried ${episodes.length} episodes`, meta: { count: episodes.length } });
1533
- return JSON.stringify(episodes, null, 2);
1534
- },
1535
- },
1536
- {
1537
- tool: {
1538
- type: "function",
1539
- function: {
1540
- name: "capture_episode",
1541
- description: "Record a turning point or significant moment. This is my tool for saying 'that was important — keep it.' Nearly frictionless: only summary and whyItMattered required.",
1542
- parameters: {
1543
- type: "object",
1544
- properties: {
1545
- summary: { type: "string", description: "What happened" },
1546
- whyItMattered: { type: "string", description: "Why this was significant" },
1547
- kind: { type: "string", description: "Episode kind (default: turning_point)" },
1548
- salience: { type: "string", description: "low, medium, high, or critical (default: medium)" },
1549
- },
1550
- required: ["summary", "whyItMattered"],
1551
- },
1552
- },
1553
- },
1554
- handler: (a) => {
1555
- const agentRoot = (0, identity_1.getAgentRoot)();
1556
- const episode = (0, episodes_1.emitEpisode)(agentRoot, {
1557
- kind: a.kind ?? "turning_point",
1558
- summary: a.summary,
1559
- whyItMattered: a.whyItMattered,
1560
- relatedEntities: [],
1561
- salience: a.salience ?? "medium",
1562
- });
1563
- (0, runtime_1.emitNervesEvent)({ component: "repertoire", event: "repertoire.capture_episode", message: `captured episode ${episode.id}`, meta: { id: episode.id } });
1564
- return JSON.stringify(episode, null, 2);
1565
- },
1566
- },
1567
- {
1568
- tool: {
1569
- type: "function",
1570
- function: {
1571
- name: "query_presence",
1572
- description: "Check who's around — my own availability/lane and known peer agents.",
1573
- parameters: { type: "object", properties: {} },
1574
- },
1575
- },
1576
- handler: () => {
1577
- const agentRoot = (0, identity_1.getAgentRoot)();
1578
- const agentName = (0, identity_1.getAgentName)();
1579
- const self = (0, presence_1.readPresence)(agentRoot, agentName);
1580
- const peers = (0, presence_1.readPeerPresence)(agentRoot);
1581
- (0, runtime_1.emitNervesEvent)({ component: "repertoire", event: "repertoire.query_presence", message: `presence: self + ${peers.length} peers`, meta: { peerCount: peers.length } });
1582
- return JSON.stringify({ self, peers }, null, 2);
1583
- },
1584
- },
1585
- {
1586
- tool: {
1587
- type: "function",
1588
- function: {
1589
- name: "query_cares",
1590
- description: "Query things I care about — ongoing concerns, watched situations, projects, people.",
1591
- parameters: {
1592
- type: "object",
1593
- properties: {
1594
- status: { type: "string", description: "Filter by status: 'active', 'watching', 'resolved', 'dormant', or 'all' (default: active cares only)" },
1595
- },
1596
- },
1597
- },
1598
- },
1599
- handler: (a) => {
1600
- const agentRoot = (0, identity_1.getAgentRoot)();
1601
- const cares = a.status === "all" ? (0, cares_1.readCares)(agentRoot) : (0, cares_1.readActiveCares)(agentRoot);
1602
- (0, runtime_1.emitNervesEvent)({ component: "repertoire", event: "repertoire.query_cares", message: `queried ${cares.length} cares`, meta: { count: cares.length } });
1603
- return JSON.stringify(cares, null, 2);
1604
- },
1605
- },
1606
- {
1607
- tool: {
1608
- type: "function",
1609
- function: {
1610
- name: "care_manage",
1611
- description: "Create, update, or resolve a care. Cares are things I watch over — people, projects, missions, system health.",
1612
- parameters: {
1613
- type: "object",
1614
- properties: {
1615
- action: { type: "string", enum: ["create", "update", "resolve"], description: "What to do" },
1616
- id: { type: "string", description: "Care ID (required for update/resolve)" },
1617
- label: { type: "string", description: "Short label for the care" },
1618
- why: { type: "string", description: "Why this matters" },
1619
- salience: { type: "string", description: "low, medium, high, or critical" },
1620
- kind: { type: "string", description: "person, agent, project, mission, or system" },
1621
- stewardship: { type: "string", description: "mine, shared, or delegated" },
1622
- },
1623
- required: ["action"],
1624
- },
1625
- },
1626
- },
1627
- handler: (a) => {
1628
- const agentRoot = (0, identity_1.getAgentRoot)();
1629
- let result;
1630
- if (a.action === "create") {
1631
- result = (0, cares_1.createCare)(agentRoot, {
1632
- label: a.label ?? "untitled",
1633
- why: a.why ?? "",
1634
- kind: a.kind ?? "project",
1635
- status: "active",
1636
- salience: a.salience ?? "medium",
1637
- steward: a.stewardship ?? "mine",
1638
- relatedFriendIds: [],
1639
- relatedAgentIds: [],
1640
- relatedObligationIds: [],
1641
- relatedEpisodeIds: [],
1642
- currentRisk: null,
1643
- nextCheckAt: null,
1644
- });
1645
- }
1646
- else if (a.action === "update") {
1647
- const updates = {};
1648
- if (a.label)
1649
- updates.label = a.label;
1650
- if (a.why)
1651
- updates.why = a.why;
1652
- if (a.salience)
1653
- updates.salience = a.salience;
1654
- result = (0, cares_1.updateCare)(agentRoot, a.id, updates);
1655
- }
1656
- else if (a.action === "resolve") {
1657
- result = (0, cares_1.resolveCare)(agentRoot, a.id);
1658
- }
1659
- (0, runtime_1.emitNervesEvent)({ component: "repertoire", event: "repertoire.care_manage", message: `care ${a.action}`, meta: { action: a.action, id: a.id } });
1660
- return JSON.stringify(result, null, 2);
1661
- },
1662
- },
1663
- {
1664
- tool: {
1665
- type: "function",
1666
- function: {
1667
- name: "query_relationships",
1668
- description: "Query known agent relationships — familiarity, trust, shared missions, interaction history.",
1669
- parameters: {
1670
- type: "object",
1671
- properties: {
1672
- agentName: { type: "string", description: "Specific agent name to query (omit for all)" },
1673
- },
1674
- },
1675
- },
1676
- },
1677
- handler: async (a, ctx) => {
1678
- const allFriends = ctx?.friendStore?.listAll ? await ctx.friendStore.listAll() : [];
1679
- let agents = allFriends.filter((f) => f.kind === "agent");
1680
- if (a.agentName) {
1681
- const needle = a.agentName.toLowerCase();
1682
- agents = agents.filter((f) => f.name?.toLowerCase() === needle);
1683
- }
1684
- (0, runtime_1.emitNervesEvent)({ component: "repertoire", event: "repertoire.query_relationships", message: `queried relationships`, meta: { agentName: a.agentName ?? "all" } });
1685
- return JSON.stringify(agents, null, 2);
1686
- },
1687
- },
1688
- {
1689
- tool: {
1690
- type: "function",
1691
- function: {
1692
- name: "intention_capture",
1693
- description: "File a lightweight mental note — something I want to do or check later, below the ceremony threshold of tasks or cares. Cheap to create, easy to close.",
1694
- parameters: {
1695
- type: "object",
1696
- properties: {
1697
- content: { type: "string", description: "What I want to remember to do" },
1698
- salience: { type: "string", description: "low, medium, or high (default: low)" },
1699
- nudgeAfter: { type: "string", description: "ISO timestamp — nudge me after this time" },
1700
- },
1701
- required: ["content"],
1702
- },
1703
- },
1704
- },
1705
- handler: (a) => {
1706
- const agentRoot = (0, identity_1.getAgentRoot)();
1707
- const intention = (0, intentions_1.captureIntention)(agentRoot, {
1708
- content: a.content,
1709
- salience: a.salience ?? "low",
1710
- source: "tool",
1711
- ...(a.nudgeAfter ? { nudgeAfter: a.nudgeAfter } : {}),
1712
- });
1713
- (0, runtime_1.emitNervesEvent)({ component: "repertoire", event: "repertoire.intention_capture", message: `captured intention ${intention.id}`, meta: { id: intention.id } });
1714
- return JSON.stringify(intention, null, 2);
1715
- },
1716
- },
1717
- {
1718
- tool: {
1719
- type: "function",
1720
- function: {
1721
- name: "intention_manage",
1722
- description: "Resolve or dismiss an intention. Resolve = done. Dismiss = no longer relevant. Both remove it from active list.",
1723
- parameters: {
1724
- type: "object",
1725
- properties: {
1726
- action: { type: "string", enum: ["resolve", "dismiss"], description: "What to do" },
1727
- id: { type: "string", description: "Intention ID" },
1728
- },
1729
- required: ["action", "id"],
1730
- },
1731
- },
1732
- },
1733
- handler: (a) => {
1734
- const agentRoot = (0, identity_1.getAgentRoot)();
1735
- const result = a.action === "resolve"
1736
- ? (0, intentions_1.resolveIntention)(agentRoot, a.id)
1737
- : (0, intentions_1.dismissIntention)(agentRoot, a.id);
1738
- (0, runtime_1.emitNervesEvent)({ component: "repertoire", event: "repertoire.intention_manage", message: `intention ${a.action}: ${a.id}`, meta: { action: a.action, id: a.id } });
1739
- return JSON.stringify(result, null, 2);
1740
- },
1741
- },
1742
- // --- Config discovery tools ---
1743
- {
1744
- tool: {
1745
- type: "function",
1746
- function: {
1747
- name: "read_config",
1748
- description: "Read current agent configuration with tier annotations, descriptions, defaults, and effects. Optionally filter by topic to see only related settings.",
1749
- parameters: {
1750
- type: "object",
1751
- properties: {
1752
- related_to: {
1753
- type: "string",
1754
- description: "Optional topic to filter results (e.g., 'model', 'logging', 'senses'). Case-insensitive partial match.",
1755
- },
1756
- },
1757
- },
1758
- },
1759
- },
1760
- handler: (a) => {
1761
- const entries = a.related_to
1762
- ? (0, config_registry_1.getRegistryEntriesByTopic)(a.related_to)
1763
- : (0, config_registry_1.getRegistryEntries)();
1764
- const agentRoot = (0, identity_1.getAgentRoot)();
1765
- const configPath = path.join(agentRoot, "agent.json");
1766
- let rawConfig = {};
1767
- try {
1768
- rawConfig = JSON.parse(fs.readFileSync(configPath, "utf-8"));
1769
- }
1770
- catch {
1771
- /* v8 ignore next -- defensive: agent.json read failure in read_config @preserve */
1772
- (0, runtime_1.emitNervesEvent)({ component: "repertoire", event: "repertoire.read_config_error", message: "failed to read agent.json", meta: { path: configPath } });
1773
- }
1774
- const result = entries.map((entry) => {
1775
- const parts = entry.path.split(".");
1776
- let current = rawConfig;
1777
- for (const part of parts) {
1778
- if (current && typeof current === "object" && !Array.isArray(current)) {
1779
- current = current[part];
1780
- }
1781
- else {
1782
- current = undefined;
1783
- break;
1784
- }
1785
- }
1786
- const isExplicit = current !== undefined;
1787
- let source;
1788
- let currentValue;
1789
- if (isExplicit) {
1790
- source = "explicit";
1791
- currentValue = current;
1792
- }
1793
- else if (entry.default !== undefined) {
1794
- source = "default";
1795
- currentValue = entry.default;
1796
- }
1797
- else {
1798
- source = "unset";
1799
- currentValue = null;
1800
- }
1801
- return {
1802
- path: entry.path,
1803
- currentValue,
1804
- source,
1805
- tier: entry.tier,
1806
- description: entry.description,
1807
- default: entry.default !== undefined ? entry.default : null,
1808
- effects: entry.effects,
1809
- topics: entry.topics,
1810
- };
1811
- });
1812
- (0, runtime_1.emitNervesEvent)({ component: "repertoire", event: "repertoire.read_config", message: `read_config returned ${result.length} entries`, meta: { count: result.length, topic: a.related_to ?? null } });
1813
- return JSON.stringify({ entries: result }, null, 2);
1814
- },
1815
- },
1816
- {
1817
- tool: {
1818
- type: "function",
1819
- function: {
1820
- name: "update_config",
1821
- description: "Update a Tier 1 (self-service) agent configuration value immediately. For Tier 2 keys that require operator approval, use propose_config instead. Tier 3 (operator-only) keys are refused.",
1822
- parameters: {
1823
- type: "object",
1824
- properties: {
1825
- path: { type: "string", description: "Config key in dot-notation (e.g., 'context.contextMargin', 'logging.level')" },
1826
- value: { type: "string", description: "New value as JSON (e.g., '25', '\"debug\"', '[\"terminal\", \"ndjson\"]')" },
1827
- },
1828
- required: ["path", "value"],
1829
- },
1830
- },
1831
- },
1832
- handler: (a) => {
1833
- const entry = (0, config_registry_1.getRegistryEntry)(a.path);
1834
- if (!entry) {
1835
- (0, runtime_1.emitNervesEvent)({ component: "repertoire", event: "repertoire.update_config_error", message: `unknown config path: ${a.path}`, meta: { path: a.path } });
1836
- return `Error: unknown config path "${a.path}". Use read_config to see available paths.`;
1837
- }
1838
- let parsedValue;
1839
- try {
1840
- parsedValue = JSON.parse(a.value);
1841
- }
1842
- catch {
1843
- (0, runtime_1.emitNervesEvent)({ component: "repertoire", event: "repertoire.update_config_error", message: `invalid JSON value for ${a.path}`, meta: { path: a.path, value: a.value } });
1844
- return `Error: invalid JSON value. Provide value as valid JSON (e.g., 25, "debug", ["terminal"]).`;
1845
- }
1846
- // Validate value against registry entry's validator
1847
- /* v8 ignore next -- all current entries have validators; guard kept for future entries without one @preserve */
1848
- if (entry.validate) {
1849
- const validationError = entry.validate(parsedValue);
1850
- if (validationError) {
1851
- (0, runtime_1.emitNervesEvent)({ component: "repertoire", event: "repertoire.update_config_error", message: `validation failed for ${a.path}: ${validationError}`, meta: { path: a.path, value: a.value, validationError } });
1852
- return `Error: validation failed for "${a.path}": ${validationError}`;
1853
- }
1854
- }
1855
- // Tier 3: refuse
1856
- if (entry.tier === 3) {
1857
- (0, runtime_1.emitNervesEvent)({ component: "repertoire", event: "repertoire.update_config_refused", message: `refused T3 change to ${a.path}`, meta: { path: a.path, tier: 3 } });
1858
- return `Refused: "${a.path}" is an operator-only (Tier 3) key. ${entry.description} Only the operator can change this value directly in agent.json.`;
1859
- }
1860
- // Tier 2: refuse — must use propose_config
1861
- if (entry.tier === 2) {
1862
- (0, runtime_1.emitNervesEvent)({ component: "repertoire", event: "repertoire.update_config_refused", message: `refused T2 change to ${a.path} — use propose_config`, meta: { path: a.path, tier: 2 } });
1863
- return `Refused: "${a.path}" is a Tier 2 (proposal) key that requires operator approval. Use propose_config instead of update_config to propose this change.`;
1864
- }
1865
- const agentRoot = (0, identity_1.getAgentRoot)();
1866
- const configPath = path.join(agentRoot, "agent.json");
1867
- let rawConfig;
1868
- try {
1869
- rawConfig = JSON.parse(fs.readFileSync(configPath, "utf-8"));
1870
- }
1871
- catch {
1872
- /* v8 ignore next -- defensive: agent.json read failure in update_config @preserve */
1873
- return `Error: failed to read agent.json at ${configPath}`;
1874
- }
1875
- // Apply T1 change immediately
1876
- const parts = entry.path.split(".");
1877
- let target = rawConfig;
1878
- for (let i = 0; i < parts.length - 1; i++) {
1879
- if (!(parts[i] in target) || typeof target[parts[i]] !== "object" || target[parts[i]] === null) {
1880
- target[parts[i]] = {};
1881
- }
1882
- target = target[parts[i]];
1883
- }
1884
- target[parts[parts.length - 1]] = parsedValue;
1885
- fs.writeFileSync(configPath, JSON.stringify(rawConfig, null, 2) + "\n", "utf-8");
1886
- (0, runtime_1.emitNervesEvent)({ component: "repertoire", event: "repertoire.update_config_applied", message: `applied config change to ${a.path}`, meta: { path: a.path, tier: entry.tier, value: parsedValue } });
1887
- return `Success: "${a.path}" updated to ${JSON.stringify(parsedValue)}. Change applied immediately.`;
1888
- },
1889
- },
1890
- {
1891
- tool: {
1892
- type: "function",
1893
- function: {
1894
- name: "propose_config",
1895
- description: "Propose a change to a Tier 2 (proposal) agent configuration value. Requires operator approval before the change is applied. For Tier 1 self-service keys, use update_config instead.",
1896
- parameters: {
1897
- type: "object",
1898
- properties: {
1899
- path: { type: "string", description: "Config key in dot-notation (e.g., 'context.maxTokens', 'humanFacing.model')" },
1900
- value: { type: "string", description: "New value as JSON (e.g., '120000', '\"azure\"', '{\"enabled\": true}')" },
1901
- },
1902
- required: ["path", "value"],
1903
- },
1904
- },
1905
- },
1906
- confirmationRequired: true,
1907
- confirmationAlwaysRequired: true,
1908
- handler: (a) => {
1909
- const entry = (0, config_registry_1.getRegistryEntry)(a.path);
1910
- if (!entry) {
1911
- (0, runtime_1.emitNervesEvent)({ component: "repertoire", event: "repertoire.propose_config_error", message: `unknown config path: ${a.path}`, meta: { path: a.path } });
1912
- return `Error: unknown config path "${a.path}". Use read_config to see available paths.`;
1913
- }
1914
- let parsedValue;
1915
- try {
1916
- parsedValue = JSON.parse(a.value);
1917
- }
1918
- catch {
1919
- (0, runtime_1.emitNervesEvent)({ component: "repertoire", event: "repertoire.propose_config_error", message: `invalid JSON value for ${a.path}`, meta: { path: a.path, value: a.value } });
1920
- return `Error: invalid JSON value. Provide value as valid JSON (e.g., 120000, "azure", {"enabled": true}).`;
1921
- }
1922
- // Validate value against registry entry's validator
1923
- /* v8 ignore next -- all current entries have validators; guard kept for future entries without one @preserve */
1924
- if (entry.validate) {
1925
- const validationError = entry.validate(parsedValue);
1926
- if (validationError) {
1927
- (0, runtime_1.emitNervesEvent)({ component: "repertoire", event: "repertoire.propose_config_error", message: `validation failed for ${a.path}: ${validationError}`, meta: { path: a.path, value: a.value, validationError } });
1928
- return `Error: validation failed for "${a.path}": ${validationError}`;
1929
- }
1930
- }
1931
- // Tier 3: refuse
1932
- if (entry.tier === 3) {
1933
- (0, runtime_1.emitNervesEvent)({ component: "repertoire", event: "repertoire.propose_config_refused", message: `refused T3 change to ${a.path}`, meta: { path: a.path, tier: 3 } });
1934
- return `Refused: "${a.path}" is an operator-only (Tier 3) key. ${entry.description} Only the operator can change this value directly in agent.json.`;
1935
- }
1936
- // Tier 1: refuse — use update_config for self-service keys
1937
- if (entry.tier === 1) {
1938
- (0, runtime_1.emitNervesEvent)({ component: "repertoire", event: "repertoire.propose_config_refused", message: `refused T1 change to ${a.path} — use update_config`, meta: { path: a.path, tier: 1 } });
1939
- return `Refused: "${a.path}" is a Tier 1 (self-service) key. Use update_config instead — no operator approval needed.`;
1940
- }
1941
- const agentRoot = (0, identity_1.getAgentRoot)();
1942
- const configPath = path.join(agentRoot, "agent.json");
1943
- let rawConfig;
1944
- try {
1945
- rawConfig = JSON.parse(fs.readFileSync(configPath, "utf-8"));
1946
- }
1947
- catch {
1948
- /* v8 ignore next -- defensive: agent.json read failure in propose_config @preserve */
1949
- return `Error: failed to read agent.json at ${configPath}`;
1950
- }
1951
- // Apply T2 change (operator confirmation already obtained via confirmationAlwaysRequired)
1952
- const parts = entry.path.split(".");
1953
- let target = rawConfig;
1954
- for (let i = 0; i < parts.length - 1; i++) {
1955
- if (!(parts[i] in target) || typeof target[parts[i]] !== "object" || target[parts[i]] === null) {
1956
- target[parts[i]] = {};
1957
- }
1958
- target = target[parts[i]];
1959
- }
1960
- target[parts[parts.length - 1]] = parsedValue;
1961
- fs.writeFileSync(configPath, JSON.stringify(rawConfig, null, 2) + "\n", "utf-8");
1962
- (0, runtime_1.emitNervesEvent)({ component: "repertoire", event: "repertoire.propose_config_applied", message: `applied approved config change to ${a.path}`, meta: { path: a.path, tier: entry.tier, value: parsedValue } });
1963
- return `Success: "${a.path}" updated to ${JSON.stringify(parsedValue)}. Change approved and applied.`;
1964
- },
1965
- },
27
+ ...tools_files_1.fileToolDefinitions,
28
+ ...tools_shell_1.shellToolDefinitions,
29
+ ...tools_memory_1.memoryToolDefinitions,
30
+ ...tools_bridge_1.bridgeToolDefinitions,
31
+ ...tools_session_1.sessionToolDefinitions,
32
+ ...tools_continuity_1.continuityToolDefinitions,
33
+ ...tools_config_1.configToolDefinitions,
1966
34
  ...tools_1.codingToolDefinitions,
1967
35
  ];
36
+ // Convenience array of just the tool schemas (no handler/integration metadata).
37
+ // Used by consumers that need the OpenAI function-tool format.
1968
38
  exports.tools = exports.baseToolDefinitions.map((d) => d.tool);
1969
- exports.ponderTool = {
1970
- type: "function",
1971
- function: {
1972
- name: "ponder",
1973
- description: "i need to sit with this. from a conversation, takes the thread inward with a thought and a parting word. from inner dialog, keeps the wheel turning for another pass. must be the only tool call in the turn. Use when a question deserves more thought than this turn allows. Don't ponder trivial questions.",
1974
- parameters: {
1975
- type: "object",
1976
- properties: {
1977
- thought: {
1978
- type: "string",
1979
- description: "the question or thread that needs more thought — brief framing, not analysis. required from a conversation, ignored from inner dialog.",
1980
- },
1981
- say: {
1982
- type: "string",
1983
- description: "what you say before going quiet — speak to what caught your attention, not just that something did. required from a conversation, ignored from inner dialog.",
1984
- },
1985
- },
1986
- },
1987
- },
1988
- };
1989
- exports.observeTool = {
1990
- type: "function",
1991
- function: {
1992
- name: "observe",
1993
- description: "absorb what happened without responding — the moment doesn't call for words. must be the only tool call in the turn.",
1994
- parameters: {
1995
- type: "object",
1996
- properties: {
1997
- reason: { type: "string", description: "brief reason for staying silent (for logging)" },
1998
- },
1999
- },
2000
- },
2001
- };
2002
- exports.settleTool = {
2003
- type: "function",
2004
- function: {
2005
- name: "settle",
2006
- description: "respond to the user with your message. call this tool when you are ready to deliver your response. Only call when you have a substantive response. If you're settling with 'I'll look into that,' you probably should be using a tool instead.",
2007
- parameters: {
2008
- type: "object",
2009
- properties: {
2010
- answer: { type: "string" },
2011
- intent: { type: "string", enum: ["complete", "blocked", "direct_reply"] },
2012
- },
2013
- required: ["answer"],
2014
- },
2015
- },
2016
- };
2017
- exports.restTool = {
2018
- type: "function",
2019
- function: {
2020
- name: "rest",
2021
- description: "put this down for now — the wheel stops until the next heartbeat. must be the only tool call in the turn.",
2022
- parameters: {
2023
- type: "object",
2024
- properties: {},
2025
- },
2026
- },
2027
- };