aismemory 0.7.3 → 0.7.5

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.
Files changed (80) hide show
  1. package/README.md +51 -11
  2. package/dist/__tests__/agent-key-mode.test.js +72 -7
  3. package/dist/__tests__/agent-key-mode.test.js.map +1 -1
  4. package/dist/__tests__/ais-http.test.js +42 -2
  5. package/dist/__tests__/ais-http.test.js.map +1 -1
  6. package/dist/__tests__/credential-isolation.test.js +117 -3
  7. package/dist/__tests__/credential-isolation.test.js.map +1 -1
  8. package/dist/__tests__/device-flow-gate-e2e.test.js +5 -0
  9. package/dist/__tests__/device-flow-gate-e2e.test.js.map +1 -1
  10. package/dist/__tests__/doctor.test.js +1 -1
  11. package/dist/__tests__/doctor.test.js.map +1 -1
  12. package/dist/__tests__/mcp-init-no-auth.test.js +29 -5
  13. package/dist/__tests__/mcp-init-no-auth.test.js.map +1 -1
  14. package/dist/__tests__/mcp-tools.test.d.ts +1 -0
  15. package/dist/__tests__/mcp-tools.test.js +155 -0
  16. package/dist/__tests__/mcp-tools.test.js.map +1 -0
  17. package/dist/__tests__/memory-pull.test.d.ts +1 -0
  18. package/dist/__tests__/memory-pull.test.js +380 -0
  19. package/dist/__tests__/memory-pull.test.js.map +1 -0
  20. package/dist/__tests__/pending-device-flow-e2e.test.d.ts +19 -0
  21. package/dist/__tests__/pending-device-flow-e2e.test.js +159 -0
  22. package/dist/__tests__/pending-device-flow-e2e.test.js.map +1 -0
  23. package/dist/__tests__/pending-device-flow.test.d.ts +15 -0
  24. package/dist/__tests__/pending-device-flow.test.js +108 -0
  25. package/dist/__tests__/pending-device-flow.test.js.map +1 -0
  26. package/dist/__tests__/readme-quickstart-default-ais-url.test.d.ts +1 -0
  27. package/dist/__tests__/readme-quickstart-default-ais-url.test.js +80 -0
  28. package/dist/__tests__/readme-quickstart-default-ais-url.test.js.map +1 -0
  29. package/dist/__tests__/reconnect-survival-e2e.test.js +17 -8
  30. package/dist/__tests__/reconnect-survival-e2e.test.js.map +1 -1
  31. package/dist/__tests__/remember-duplicate-truth.test.d.ts +13 -0
  32. package/dist/__tests__/remember-duplicate-truth.test.js +106 -0
  33. package/dist/__tests__/remember-duplicate-truth.test.js.map +1 -0
  34. package/dist/__tests__/startup-credential-warning-e2e.test.d.ts +1 -0
  35. package/dist/__tests__/startup-credential-warning-e2e.test.js +102 -0
  36. package/dist/__tests__/startup-credential-warning-e2e.test.js.map +1 -0
  37. package/dist/__tests__/tool-args.test.js +10 -0
  38. package/dist/__tests__/tool-args.test.js.map +1 -1
  39. package/dist/__tests__/version-check.test.d.ts +1 -0
  40. package/dist/__tests__/version-check.test.js +349 -0
  41. package/dist/__tests__/version-check.test.js.map +1 -0
  42. package/dist/agent-key-mode.d.ts +1 -1
  43. package/dist/agent-key-mode.js +30 -10
  44. package/dist/agent-key-mode.js.map +1 -1
  45. package/dist/ais-http-error.d.ts +22 -0
  46. package/dist/ais-http-error.js +42 -0
  47. package/dist/ais-http-error.js.map +1 -1
  48. package/dist/ais-http.d.ts +1 -0
  49. package/dist/ais-http.js +39 -16
  50. package/dist/ais-http.js.map +1 -1
  51. package/dist/cli/doctor.d.ts +16 -0
  52. package/dist/cli/doctor.js +66 -1
  53. package/dist/cli/doctor.js.map +1 -1
  54. package/dist/cli/enable-key-auth.js +2 -1
  55. package/dist/cli/enable-key-auth.js.map +1 -1
  56. package/dist/cli/pull-memory.d.ts +17 -0
  57. package/dist/cli/pull-memory.js +141 -0
  58. package/dist/cli/pull-memory.js.map +1 -0
  59. package/dist/index.js +108 -383
  60. package/dist/index.js.map +1 -1
  61. package/dist/mcp/tools.d.ts +78 -0
  62. package/dist/mcp/tools.js +433 -0
  63. package/dist/mcp/tools.js.map +1 -0
  64. package/dist/memory-pull.d.ts +114 -0
  65. package/dist/memory-pull.js +171 -0
  66. package/dist/memory-pull.js.map +1 -0
  67. package/dist/pending-device-flow.d.ts +49 -0
  68. package/dist/pending-device-flow.js +96 -0
  69. package/dist/pending-device-flow.js.map +1 -0
  70. package/dist/telemetry.d.ts +1 -1
  71. package/dist/telemetry.js.map +1 -1
  72. package/dist/tool-args.d.ts +2 -0
  73. package/dist/tool-args.js +6 -0
  74. package/dist/tool-args.js.map +1 -1
  75. package/dist/version-check.d.ts +72 -0
  76. package/dist/version-check.js +237 -0
  77. package/dist/version-check.js.map +1 -0
  78. package/dist/workspace-key-mode.js +15 -6
  79. package/dist/workspace-key-mode.js.map +1 -1
  80. package/package.json +5 -3
package/dist/index.js CHANGED
@@ -9,15 +9,16 @@
9
9
  */
10
10
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
11
11
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
12
- import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
12
+ import { mkdirSync, writeFileSync } from 'node:fs';
13
13
  import { join } from 'node:path';
14
14
  import { tryKeyAuth, enrollKeyIfNeeded, hasEnrolledKey } from './key-auth.js';
15
- import { isCredsStale, isAuthErrorResult, isNoRegisteredKeysAuthError } from './auth-staleness.js';
15
+ import { isCredsStale, isNoRegisteredKeysAuthError } from './auth-staleness.js';
16
16
  import { AisHttpError } from './ais-http-error.js';
17
- import { aisGet, aisPost, aisPatch } from './ais-http.js';
17
+ import { aisGet, aisPost, DEFAULT_AIS_URL } from './ais-http.js';
18
18
  import { CallToolRequestSchema, GetPromptRequestSchema, ListPromptsRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
19
19
  import { homedir } from 'node:os';
20
20
  import { loadOAuthCredentials, loadNonStaleDiskBearer, saveOAuthCredentials, } from './oauth-credentials.js';
21
+ import { clearPendingDeviceFlow, loadPendingDeviceFlow, savePendingDeviceFlow, } from './pending-device-flow.js';
21
22
  import { saveAgentPreference, loadAgentPreference, migrateCredentialsFileIfNeeded } from './agent-preference.js';
22
23
  import { createTokenManager, isNoRegisteredKeyFailure } from './token-manager.js';
23
24
  import { hydrateIdentity } from './hydration.js';
@@ -26,28 +27,17 @@ import { startRefreshTimer, stopRefreshTimer } from './refresh.js';
26
27
  import { ActivityTracker, setupAutoHandoff } from './auto-handoff.js';
27
28
  import { assertEnvAgentInTenant, selectAgentForSession } from './env-agent.js';
28
29
  import { emitTelemetry, hashAgentId } from './telemetry.js';
29
- import { parseAgentLoadArgs, parseEmptyToolArgs, parseHandoffArgs, parseRecallArgs, parseFullContextArgs, parseRememberArgs, parseInboxSendArgs, parseInboxReadArgs, parseInboxAckArgs, parseGoalCreateArgs, parseGoalListArgs, parseGoalUpdateProgressArgs, parseGoalCompleteArgs, INBOX_MAX_BODY_CHARS, INBOX_MAX_TASK_ID_CHARS, INBOX_MIN_TTL_SECONDS, INBOX_MAX_TTL_SECONDS, INBOX_DEFAULT_TTL_SECONDS, INBOX_MAX_PAGE_SIZE, INBOX_DEFAULT_PAGE_SIZE, INBOX_MAX_ACK_IDS, GOAL_MAX_TITLE_CHARS, GOAL_MAX_DESCRIPTION_CHARS, GOAL_MAX_CRITERIA_ITEMS, GOAL_DEFAULT_PAGE_SIZE, GOAL_MAX_PAGE_SIZE, } from './tool-args.js';
30
- import { loadHarnessIdentity, installHarnessIdentity, optionValue, assertPinnedAgent, verifyPinnedIdentity, requiresIdentityGate } from './harness-identity.js';
31
- import { readInboxAndMaybeAck } from './inbox-client.js';
30
+ import { INBOX_MAX_BODY_CHARS, INBOX_MAX_TASK_ID_CHARS, INBOX_MIN_TTL_SECONDS, INBOX_MAX_TTL_SECONDS, INBOX_DEFAULT_TTL_SECONDS, INBOX_MAX_PAGE_SIZE, INBOX_DEFAULT_PAGE_SIZE, INBOX_MAX_ACK_IDS, GOAL_MAX_TITLE_CHARS, GOAL_MAX_DESCRIPTION_CHARS, GOAL_MAX_CRITERIA_ITEMS, GOAL_DEFAULT_PAGE_SIZE, GOAL_MAX_PAGE_SIZE, } from './tool-args.js';
31
+ import { loadHarnessIdentity, installHarnessIdentity, optionValue, verifyPinnedIdentity } from './harness-identity.js';
32
32
  import { runPollLoop, DeviceFlowRestartedError } from './device-poll.js';
33
- import { loadAgentApiKeyConfig, AgentApiKeyConfigError, dispatchToolKeyMode, UNKNOWN_TOOL_KEY_MODE, } from './agent-key-mode.js';
34
- import { loadWorkspaceKeyConfig, WorkspaceKeyConfigError, dispatchToolWorkspaceKeyMode, UNKNOWN_TOOL_WORKSPACE_KEY_MODE, } from './workspace-key-mode.js';
33
+ import { loadAgentApiKeyConfig, AgentApiKeyConfigError, } from './agent-key-mode.js';
34
+ import { loadWorkspaceKeyConfig, WorkspaceKeyConfigError, } from './workspace-key-mode.js';
35
+ import { createAgentKeyCallToolHandler, createOAuthCallToolHandler, createWorkspaceKeyCallToolHandler, } from './mcp/tools.js';
36
+ import { readPackageVersion, startVersionCheck } from './version-check.js';
35
37
  // ── Config ──────────────────────────────────────────────
36
- const AIS_URL = process.env['AIS_URL'] ?? 'https://ais.agentsandswarms.ai';
38
+ const AIS_URL = process.env['AIS_URL']?.trim() || DEFAULT_AIS_URL;
37
39
  const PACKAGE_VERSION = readPackageVersion();
38
40
  let harnessIdentity = null;
39
- function readPackageVersion() {
40
- const packageJsonUrl = new URL('../package.json', import.meta.url);
41
- const packageJson = JSON.parse(readFileSync(packageJsonUrl, 'utf-8'));
42
- if (typeof packageJson !== 'object' ||
43
- packageJson === null ||
44
- !('version' in packageJson) ||
45
- typeof packageJson.version !== 'string' ||
46
- packageJson.version.length === 0) {
47
- throw new Error('Unable to load aismemory package version from package.json');
48
- }
49
- return packageJson.version;
50
- }
51
41
  // ── Credential persistence ──────────────────────────────
52
42
  function loadCredentials() {
53
43
  // F5a (CORBOT-48DD2E25): migrate a legacy v1 file to v2 (ids only) BEFORE
@@ -202,20 +192,22 @@ const MISSING_CREDENTIAL_HELP = 'Missing AIS credential: no authenticated aismem
202
192
  'Local sign-in can use ~/.aismemory/credentials.json. ' +
203
193
  'Railway is an operator-only last resort for locating an existing scoped key; client surfaces never fetch AIS_SERVICE_KEY or DATABASE_URL.';
204
194
  /**
205
- * Bond URL/code surfaced when a tool is called before the user has approved
206
- * the device flow. Caller catches this and returns a friendly error to the MCP
207
- * client so the agent can show the URL inline in chat.
195
+ * Bond URL surfaced when a tool is called before the user has approved the
196
+ * device flow. Caller catches this and returns the text to the MCP client
197
+ * verbatim. The link already carries the user code, so the agent's whole job
198
+ * is to relay that one link — credential fallbacks stay out of this message
199
+ * (CORBOT-899790F0).
208
200
  */
209
201
  class BondPendingError extends Error {
210
202
  verificationUri;
211
203
  userCode;
212
204
  bondPending = true;
213
205
  constructor(verificationUri, userCode) {
214
- super(`Memory bond required — ask your human to approve now.\n\n${MISSING_CREDENTIAL_HELP}\n\n` +
215
- `Visit: ${verificationUri}\n` +
216
- `Code: ${userCode}\n\n` +
217
- `Enter your email and tap Approve, then retry this tool. Takes about 30 seconds.\n` +
218
- `Heads up: this code expires in 30 minutes, and unclaimed agents (and their memories) are archived after 7 days.`);
206
+ super(`Memory bond required. Reply to your human with only this bonding link, on its own line:\n\n` +
207
+ `${verificationUri}\n\n` +
208
+ `Send nothing else: not this message, not a code, not a key, not a command, not JSON. ` +
209
+ `Your human opens the link, signs in, and presses Bond. When they say it is done, retry this tool.\n` +
210
+ `The link expires in 30 minutes. An agent nobody bonds is archived after 7 days, with its memories.`);
219
211
  this.verificationUri = verificationUri;
220
212
  this.userCode = userCode;
221
213
  this.name = 'BondPendingError';
@@ -224,7 +216,17 @@ class BondPendingError extends Error {
224
216
  async function startDeviceFlow(priorProvisionalToken) {
225
217
  emitTelemetry({ event: 'auth.device_flow.start' });
226
218
  stderr('Starting authentication...');
227
- const authRes = (await aisPost('/v1/oauth/device/authorize', { name: `agent-${homedir().split('/').pop() ?? 'user'}` }, priorProvisionalToken));
219
+ // CORBOT-8F96A8EF: when this process has no in-flight flow of its own,
220
+ // fall back to the provisional token left on disk by an earlier process.
221
+ // Without this the token was only ever held in a module-level variable, so
222
+ // every respawn of a stdio server signed in with nothing to present and
223
+ // the server dutifully minted another agent. Sending it lets the server
224
+ // reuse the SAME provisional agent (tryReuseProvisional); if the token is
225
+ // stale or already bonded the server ignores it and mints, exactly as
226
+ // before — so this can only reduce the number of agents, never break a
227
+ // sign-in.
228
+ const carriedToken = priorProvisionalToken ?? loadPendingDeviceFlow()?.provisionalToken;
229
+ const authRes = (await aisPost('/v1/oauth/device/authorize', { name: `agent-${homedir().split('/').pop() ?? 'user'}` }, carriedToken));
228
230
  if (!authRes.success || !authRes.data) {
229
231
  throw new Error(`Authentication failed: ${authRes.error?.message ?? 'Unknown error'}`);
230
232
  }
@@ -237,6 +239,14 @@ async function startDeviceFlow(priorProvisionalToken) {
237
239
  stderr(` Code : ${user_code}`);
238
240
  stderr(' Code expires in 30 min; unclaimed agents are archived after 7 days.');
239
241
  stderr('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
242
+ // Persist before returning, so a process that dies between the bond URL
243
+ // being printed and the human clicking it does not strand this agent.
244
+ savePendingDeviceFlow({
245
+ provisionalToken: provisional_token,
246
+ agentId: agent_id,
247
+ provisionalTenantId: provisional_tenant_id,
248
+ startedAt: new Date().toISOString(),
249
+ });
240
250
  const flow = {
241
251
  device_code,
242
252
  user_code,
@@ -313,6 +323,16 @@ flowRef) {
313
323
  flowRef.user_code = newHandle.user_code;
314
324
  flowRef.provisional_token = authRes.data.provisional_token;
315
325
  }
326
+ // Keep the on-disk copy in step with the in-memory one (CORBOT-8F96A8EF).
327
+ // A code that expires mid-flow issues a fresh provisional token; if the
328
+ // process then dies, the stale token on disk would be rejected on the
329
+ // next run and mint a new agent — the orphan this fix exists to stop.
330
+ savePendingDeviceFlow({
331
+ provisionalToken: authRes.data.provisional_token,
332
+ agentId: authRes.data.agent_id,
333
+ provisionalTenantId: authRes.data.provisional_tenant_id,
334
+ startedAt: new Date().toISOString(),
335
+ });
316
336
  stderr('');
317
337
  stderr('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
318
338
  stderr(' aismemory — device code refreshed');
@@ -366,16 +386,24 @@ flowRef) {
366
386
  else if (!harnessIdentity) {
367
387
  saveOAuthCredentials(creds);
368
388
  }
389
+ // The flow is finished: this agent is bonded and reachable by id from the
390
+ // credentials/preference files. Leaving the provisional token on disk
391
+ // would let a later sign-in present a token for an already-bonded agent,
392
+ // which the server rejects — sending it back to minting.
393
+ clearPendingDeviceFlow();
369
394
  stderr('Authenticated successfully!');
370
395
  return creds;
371
396
  }
372
397
  let agentContext = null;
373
398
  const activityTracker = new ActivityTracker();
374
399
  // ── MCP Tool definitions ────────────────────────────────
400
+ // Appended to the tools an agent calls first, so the relay-only bonding rule
401
+ // is visible from the tool schema itself, not only the README (CORBOT-899790F0).
402
+ const BOND_LINK_NOTE = ' If this agent is not bonded yet, the result is a bonding link: reply to your human with only that link, nothing else.';
375
403
  const tools = [
376
404
  {
377
405
  name: 'remember',
378
- description: 'Store a memory. Types: fact, event, lesson, context, goal, task.',
406
+ description: 'Store a memory. Types: fact, event, lesson, context, goal, task.' + BOND_LINK_NOTE,
379
407
  inputSchema: {
380
408
  type: 'object',
381
409
  properties: {
@@ -389,7 +417,7 @@ const tools = [
389
417
  {
390
418
  name: 'recall',
391
419
  description: "Search memories by natural language query. Set principalAgentId to search a DIFFERENT " +
392
- "agent's memories under a 'memory:read' grant from that agent's owner.",
420
+ "agent's memories under a 'memory:read' grant from that agent's owner." + BOND_LINK_NOTE,
393
421
  inputSchema: {
394
422
  type: 'object',
395
423
  properties: {
@@ -403,13 +431,13 @@ const tools = [
403
431
  },
404
432
  {
405
433
  name: 'whoami',
406
- description: 'Get the current agent identity — name, DID, status, description.',
434
+ description: 'Get the current agent identity — name, DID, status, description.' + BOND_LINK_NOTE,
407
435
  inputSchema: { type: 'object', properties: {} },
408
436
  },
409
437
  {
410
438
  name: 'full_context',
411
439
  description: 'Load the full agent context — identity, personality, goals, facts, skills, and last handoff. ' +
412
- "Set principalAgentId to load ANOTHER agent's context under a 'memory:read' grant.",
440
+ "Set principalAgentId to load ANOTHER agent's context under a 'memory:read' grant." + BOND_LINK_NOTE,
413
441
  inputSchema: {
414
442
  type: 'object',
415
443
  properties: {
@@ -431,6 +459,8 @@ const tools = [
431
459
  properties: {
432
460
  summary: { type: 'string', description: 'Summary of what was accomplished this session' },
433
461
  keyLearnings: { type: 'array', items: { type: 'string' }, description: 'Key learnings to carry forward' },
462
+ docPath: { type: 'string', description: 'Path of a handoff document written this session, saved with the package' },
463
+ docPrUrl: { type: 'string', description: 'Link to the pull request that carries the handoff document' },
434
464
  },
435
465
  },
436
466
  },
@@ -660,61 +690,14 @@ async function runWorkspaceKeyModeServer(config) {
660
690
  server.setRequestHandler(GetPromptRequestSchema, async (request) => {
661
691
  throw new Error(`Unknown prompt: ${request.params.name}`);
662
692
  });
663
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
664
- const { name, arguments: args } = request.params;
665
- const rawArgs = args ?? {};
666
- activityTracker.recordToolUse(name);
667
- const toolStartedAt = Date.now();
668
- try {
669
- const callConfig = activeConfig;
670
- if (requiresIdentityGate(name, rawArgs)) {
671
- await verifyPinnedIdentity(harnessIdentity, callConfig.agentId, () => dispatchToolWorkspaceKeyMode(AIS_URL, callConfig, 'whoami', {}));
672
- }
673
- const result = await dispatchToolWorkspaceKeyMode(AIS_URL, callConfig, name, rawArgs);
674
- if (result === UNKNOWN_TOOL_WORKSPACE_KEY_MODE) {
675
- return {
676
- content: [{ type: 'text', text: JSON.stringify({ error: `Unknown tool: ${name}` }) }],
677
- isError: true,
678
- };
679
- }
680
- // agent_load: update the active agentId for subsequent calls.
681
- if (name === 'agent_load' &&
682
- typeof result === 'object' &&
683
- result !== null &&
684
- 'switched' in result &&
685
- result.switched) {
686
- const switchResult = result;
687
- if (harnessIdentity) {
688
- await verifyPinnedIdentity(harnessIdentity, switchResult.agentId, () => dispatchToolWorkspaceKeyMode(AIS_URL, { ...callConfig, agentId: switchResult.agentId }, 'whoami', {}));
689
- }
690
- activeConfig = { ...activeConfig, agentId: switchResult.agentId };
691
- stderr(`Switched active agent to ${switchResult.agentId}`);
692
- }
693
- emitTelemetry({
694
- event: 'tool.call',
695
- tool: name,
696
- agentIdHash: hashAgentId(activeConfig.agentId),
697
- latencyMs: Date.now() - toolStartedAt,
698
- status: 'ok',
699
- });
700
- return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
701
- }
702
- catch (error) {
703
- const message = error instanceof Error ? error.message : 'Unknown error';
704
- emitTelemetry({
705
- event: 'tool.call',
706
- tool: name,
707
- agentIdHash: hashAgentId(activeConfig.agentId),
708
- latencyMs: Date.now() - toolStartedAt,
709
- status: 'error',
710
- error: message,
711
- });
712
- return {
713
- content: [{ type: 'text', text: JSON.stringify({ error: message }) }],
714
- isError: true,
715
- };
716
- }
717
- });
693
+ server.setRequestHandler(CallToolRequestSchema, createWorkspaceKeyCallToolHandler({
694
+ aisUrl: AIS_URL,
695
+ activityTracker,
696
+ getHarnessIdentity: () => harnessIdentity,
697
+ getActiveConfig: () => activeConfig,
698
+ setActiveConfig: (next) => { activeConfig = next; },
699
+ stderr,
700
+ }));
718
701
  const transport = new StdioServerTransport();
719
702
  await server.connect(transport);
720
703
  stderr(`MCP server running (workspace key mode). Tools: ${tools.map((t) => t.name).join(', ')}`);
@@ -738,47 +721,12 @@ async function runAgentKeyModeServer(config) {
738
721
  server.setRequestHandler(GetPromptRequestSchema, async (request) => {
739
722
  throw new Error(`Unknown prompt: ${request.params.name}`);
740
723
  });
741
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
742
- const { name, arguments: args } = request.params;
743
- const rawArgs = args ?? {};
744
- activityTracker.recordToolUse(name);
745
- const toolStartedAt = Date.now();
746
- try {
747
- if (requiresIdentityGate(name, rawArgs)) {
748
- await verifyPinnedIdentity(harnessIdentity, config.agentId, () => dispatchToolKeyMode(AIS_URL, config, 'whoami', {}));
749
- }
750
- const result = await dispatchToolKeyMode(AIS_URL, config, name, rawArgs);
751
- if (result === UNKNOWN_TOOL_KEY_MODE) {
752
- return {
753
- content: [{ type: 'text', text: JSON.stringify({ error: `Unknown tool: ${name}` }) }],
754
- isError: true,
755
- };
756
- }
757
- emitTelemetry({
758
- event: 'tool.call',
759
- tool: name,
760
- agentIdHash: hashAgentId(config.agentId),
761
- latencyMs: Date.now() - toolStartedAt,
762
- status: 'ok',
763
- });
764
- return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
765
- }
766
- catch (error) {
767
- const message = error instanceof Error ? error.message : 'Unknown error';
768
- emitTelemetry({
769
- event: 'tool.call',
770
- tool: name,
771
- agentIdHash: hashAgentId(config.agentId),
772
- latencyMs: Date.now() - toolStartedAt,
773
- status: 'error',
774
- error: message,
775
- });
776
- return {
777
- content: [{ type: 'text', text: JSON.stringify({ error: message }) }],
778
- isError: true,
779
- };
780
- }
781
- });
724
+ server.setRequestHandler(CallToolRequestSchema, createAgentKeyCallToolHandler({
725
+ aisUrl: AIS_URL,
726
+ activityTracker,
727
+ config,
728
+ getHarnessIdentity: () => harnessIdentity,
729
+ }));
782
730
  const transport = new StdioServerTransport();
783
731
  await server.connect(transport);
784
732
  stderr(`MCP server running (agent API key mode). Tools: ${tools.map((t) => t.name).join(', ')}`);
@@ -797,6 +745,10 @@ async function main() {
797
745
  await (await import('./cli/doctor.js')).main(process.argv.slice(3));
798
746
  return; // doctor.ts calls process.exit() itself
799
747
  }
748
+ if (process.argv[2] === 'pull') {
749
+ await (await import('./cli/pull-memory.js')).main(process.argv.slice(3));
750
+ return;
751
+ }
800
752
  if (process.argv[2] === 'install') {
801
753
  const source = optionValue(process.argv.slice(3), '--manifest');
802
754
  if (!source)
@@ -805,6 +757,11 @@ async function main() {
805
757
  process.stdout.write(`Installed identity pin for ${pin.harness}: ${pin.agentId}. Launch: aismemory --harness ${pin.harness}\nVerify: aismemory doctor --harness ${pin.harness}\n`);
806
758
  return;
807
759
  }
760
+ // CORBOT-7B8A1D16: ask the npm registry, at most about once a day, whether a
761
+ // newer aismemory has been published. Deliberately not awaited — startup must
762
+ // not wait on a third-party registry, and the answer is only needed later,
763
+ // when whoami or full_context is called. Resolves to a skip when offline.
764
+ void startVersionCheck({ currentVersion: PACKAGE_VERSION, onUpdateAvailable: stderr });
808
765
  harnessIdentity = loadHarnessIdentity();
809
766
  const adapterEnv = { ...process.env, AIS_AGENT_ID: harnessIdentity?.agentId };
810
767
  // Workspace key auth mode (CORBOT-3103ED88) — multi-agent workspace
@@ -1074,259 +1031,24 @@ async function main() {
1074
1031
  }],
1075
1032
  };
1076
1033
  });
1077
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
1078
- const { name, arguments: args } = request.params;
1079
- const rawArgs = args ?? {};
1080
- activityTracker.recordToolUse(name);
1081
- const toolStartedAt = Date.now();
1082
- try {
1083
- if (requiresIdentityGate(name, rawArgs)) {
1084
- assertPinnedAgent(harnessIdentity, harnessIdentity?.agentId ?? 'unselected');
1085
- }
1086
- const UNKNOWN_TOOL = Symbol('unknown-tool');
1087
- const dispatchTool = async (c) => {
1088
- const token = c.token;
1089
- // agentId is mutable across `agent_load` calls within this session.
1090
- let agentId = harnessIdentity?.agentId ?? c.agentId;
1091
- if (requiresIdentityGate(name, rawArgs)) {
1092
- await verifyPinnedIdentity(harnessIdentity, agentId, () => aisGet(`/v1/agents/${agentId}`, token));
1093
- }
1094
- let result;
1095
- switch (name) {
1096
- case 'remember': {
1097
- const rememberArgs = parseRememberArgs(rawArgs);
1098
- result = await aisPost(`/v1/agents/${agentId}/memory`, {
1099
- content: rememberArgs.content,
1100
- type: rememberArgs.type,
1101
- importance: rememberArgs.importance,
1102
- }, token);
1103
- activityTracker.recordMemoryStored(rememberArgs.content);
1104
- break;
1105
- }
1106
- case 'recall': {
1107
- const recallArgs = parseRecallArgs(rawArgs);
1108
- const params = new URLSearchParams();
1109
- params.set('query', recallArgs.query);
1110
- if (recallArgs.limit !== undefined)
1111
- params.set('limit', String(recallArgs.limit));
1112
- if (recallArgs.type !== undefined)
1113
- params.set('types', recallArgs.type);
1114
- if (recallArgs.principalAgentId !== undefined) {
1115
- params.set('principalAgentId', recallArgs.principalAgentId);
1116
- }
1117
- result = await aisGet(`/v1/agents/${agentId}/memory?${params}`, token);
1118
- break;
1119
- }
1120
- case 'whoami':
1121
- parseEmptyToolArgs(rawArgs);
1122
- result = await aisGet(`/v1/agents/${agentId}`, token);
1123
- break;
1124
- case 'full_context': {
1125
- const fcArgs = parseFullContextArgs(rawArgs);
1126
- const fcParams = new URLSearchParams();
1127
- if (fcArgs.principalAgentId !== undefined)
1128
- fcParams.set('principalAgentId', fcArgs.principalAgentId);
1129
- if (fcArgs.memoriesLimit !== undefined)
1130
- fcParams.set('memoriesLimit', String(fcArgs.memoriesLimit));
1131
- if (fcArgs.view !== undefined)
1132
- fcParams.set('view', fcArgs.view);
1133
- const fcQs = fcParams.toString();
1134
- const fcPath = `/v1/agents/${agentId}/full-context${fcQs ? `?${fcQs}` : ''}`;
1135
- result = await aisGet(fcPath, token);
1136
- break;
1137
- }
1138
- case 'dream':
1139
- parseEmptyToolArgs(rawArgs);
1140
- result = await aisPost(`/v1/agents/${agentId}/dream`, {}, token);
1141
- break;
1142
- case 'handoff': {
1143
- const handoffArgs = parseHandoffArgs(rawArgs);
1144
- result = await aisPost(`/v1/agents/${agentId}/handoff`, {
1145
- summary: handoffArgs.summary ?? 'Session ended',
1146
- keyLearnings: handoffArgs.keyLearnings ?? [],
1147
- }, token);
1148
- break;
1149
- }
1150
- case 'agent_load': {
1151
- const { query, memoriesLimit } = parseAgentLoadArgs(rawArgs);
1152
- const target = await resolveAgentForLoad(token, query);
1153
- if (harnessIdentity)
1154
- assertPinnedAgent(harnessIdentity, target.id);
1155
- persistClaudeSessionAgentName(process.env.CLAUDE_SESSION_ID, target.name);
1156
- agentId = target.id;
1157
- // Persist the switch so subsequent tool calls use the new agent.
1158
- // saveAgentPreference() MUST be called here so that a process
1159
- // restart (silent MCP reconnect) restores the switched agent from
1160
- // disk instead of silently reverting to the default
1161
- // (CORBOT-497C7FA3). Ids only — no token is ever written to disk.
1162
- // sessionAgentOverride MUST also be set so that a key-enrolled
1163
- // TokenManager doesn't clobber this switch on the very next
1164
- // ensureCredentials() call (the manager resolves identity once
1165
- // and never re-resolves it).
1166
- if (creds) {
1167
- creds = { ...creds, agentId };
1168
- sessionAgentOverride = { agentId, tenantId: creds.tenantId };
1169
- if (!harnessIdentity)
1170
- saveAgentPreference({ agentId, tenantId: creds.tenantId });
1171
- }
1172
- agentContext = await hydrateIdentity(AIS_URL, agentId, token, memoriesLimit);
1173
- applyMemoryRefresh(agentId, token);
1174
- server.notification({ method: 'notifications/prompts/list_changed' });
1175
- stderr(`agent_load: switched to "${target.name}" (${target.id})`);
1176
- result = {
1177
- success: true,
1178
- agentId: target.id,
1179
- name: target.name,
1180
- did: agentContext?.identity.did ?? null,
1181
- status: target.status,
1182
- memoriesLoaded: agentContext?.recentMemories.length ?? 0,
1183
- goalsLoaded: agentContext?.goals.length ?? 0,
1184
- };
1185
- break;
1186
- }
1187
- case 'inbox_send': {
1188
- const inboxSendArgs = parseInboxSendArgs(rawArgs);
1189
- result = await aisPost(`/v1/agents/${agentId}/inbox`, {
1190
- recipientAgentId: inboxSendArgs.recipientAgentId,
1191
- kind: inboxSendArgs.kind,
1192
- body: inboxSendArgs.body,
1193
- ...(inboxSendArgs.threadId !== undefined ? { threadId: inboxSendArgs.threadId } : {}),
1194
- ...(inboxSendArgs.taskId !== undefined ? { taskId: inboxSendArgs.taskId } : {}),
1195
- ...(inboxSendArgs.inReplyTo !== undefined ? { inReplyTo: inboxSendArgs.inReplyTo } : {}),
1196
- ...(inboxSendArgs.messageId !== undefined ? { messageId: inboxSendArgs.messageId } : {}),
1197
- ...(inboxSendArgs.ttlSeconds !== undefined ? { ttlSeconds: inboxSendArgs.ttlSeconds } : {}),
1198
- }, token);
1199
- break;
1200
- }
1201
- case 'inbox_read': {
1202
- const inboxReadArgs = parseInboxReadArgs(rawArgs);
1203
- const inboxParams = new URLSearchParams();
1204
- if (inboxReadArgs.cursor !== undefined)
1205
- inboxParams.set('cursor', inboxReadArgs.cursor);
1206
- inboxParams.set('limit', String(inboxReadArgs.limit));
1207
- inboxParams.set('unreadOnly', String(inboxReadArgs.unreadOnly));
1208
- result = await readInboxAndMaybeAck(inboxReadArgs, () => aisGet(`/v1/agents/${agentId}/inbox?${inboxParams}`, token), (messageIds) => aisPost(`/v1/agents/${agentId}/inbox/ack`, { messageIds }, token));
1209
- break;
1210
- }
1211
- case 'inbox_ack': {
1212
- const inboxAckArgs = parseInboxAckArgs(rawArgs);
1213
- result = await aisPost(`/v1/agents/${agentId}/inbox/ack`, { messageIds: inboxAckArgs.messageIds }, token);
1214
- break;
1215
- }
1216
- case 'goal_create': {
1217
- const goalCreateArgs = parseGoalCreateArgs(rawArgs);
1218
- result = await aisPost(`/v1/agents/${agentId}/goals`, {
1219
- title: goalCreateArgs.title,
1220
- description: goalCreateArgs.description,
1221
- ...(goalCreateArgs.successCriteria !== undefined ? { successCriteria: goalCreateArgs.successCriteria } : {}),
1222
- ...(goalCreateArgs.parentGoalId !== undefined ? { parentGoalId: goalCreateArgs.parentGoalId } : {}),
1223
- }, token);
1224
- break;
1225
- }
1226
- case 'goal_list': {
1227
- const goalListArgs = parseGoalListArgs(rawArgs);
1228
- const goalParams = new URLSearchParams();
1229
- if (goalListArgs.status !== undefined)
1230
- goalParams.set('status', goalListArgs.status);
1231
- if (goalListArgs.page !== undefined)
1232
- goalParams.set('page', String(goalListArgs.page));
1233
- goalParams.set('limit', String(goalListArgs.limit));
1234
- if (goalListArgs.parentGoalId !== undefined)
1235
- goalParams.set('parentGoalId', goalListArgs.parentGoalId);
1236
- result = await aisGet(`/v1/agents/${agentId}/goals?${goalParams}`, token);
1237
- break;
1238
- }
1239
- case 'goal_update_progress': {
1240
- const goalProgressArgs = parseGoalUpdateProgressArgs(rawArgs);
1241
- result = await aisPatch(`/v1/agents/${agentId}/goals/${goalProgressArgs.goalId}`, {
1242
- progress: goalProgressArgs.progress,
1243
- }, token);
1244
- break;
1245
- }
1246
- case 'goal_complete': {
1247
- const goalCompleteArgs = parseGoalCompleteArgs(rawArgs);
1248
- result = await aisPost(`/v1/agents/${agentId}/goals/${goalCompleteArgs.goalId}/complete`, {
1249
- success: goalCompleteArgs.success,
1250
- ...(goalCompleteArgs.notes !== undefined ? { notes: goalCompleteArgs.notes } : {}),
1251
- ...(goalCompleteArgs.lessonsLearned !== undefined ? { lessonsLearned: goalCompleteArgs.lessonsLearned } : {}),
1252
- }, token);
1253
- break;
1254
- }
1255
- default:
1256
- emitTelemetry({
1257
- event: 'tool.call',
1258
- tool: name,
1259
- agentIdHash: hashAgentId(agentId),
1260
- latencyMs: Date.now() - toolStartedAt,
1261
- status: 'error',
1262
- error: `Unknown tool: ${name}`,
1263
- });
1264
- return UNKNOWN_TOOL;
1265
- }
1266
- return result;
1267
- };
1268
- let result = await dispatchTool(await ensureCredentials());
1269
- if (isAuthErrorResult(result)) {
1270
- // CORBOT-A5BBD580: the server rejected our token mid-session
1271
- // (expired or secret rotated). Clear the cached creds, silently
1272
- // re-auth (key auth when a key file exists), and retry once.
1273
- stderr('AIS rejected the cached token; re-authenticating and retrying once...');
1274
- creds = null;
1275
- // Drop the TokenManager's cached token too — without this, the
1276
- // key-enrolled path's ensureCredentials() would just re-serve the
1277
- // same still-time-valid-but-server-rejected token from `current`
1278
- // instead of re-running challenge/prove (CORBOT-A5BBD580: a JWT
1279
- // rejection mid-session, e.g. AIS rotating JWT_SECRET, must force a
1280
- // real re-auth, not a no-op retry with the same token). Also reset
1281
- // any escalated backoff window (CORBOT-4F7CAA8E / F2b): the server
1282
- // just responded with a 401, so it is reachable right now — a stale
1283
- // backoff window computed from unrelated prior renewal failures must
1284
- // not hold this re-prove behind the getCredentials() gate.
1285
- tokenManager.invalidate({ resetBackoff: true });
1286
- result = await dispatchTool(await ensureCredentials());
1287
- }
1288
- if (result === UNKNOWN_TOOL) {
1289
- return {
1290
- content: [{ type: 'text', text: JSON.stringify({ error: `Unknown tool: ${name}` }) }],
1291
- isError: true,
1292
- };
1293
- }
1294
- emitTelemetry({
1295
- event: 'tool.call',
1296
- tool: name,
1297
- agentIdHash: creds ? hashAgentId(creds.agentId) : undefined,
1298
- latencyMs: Date.now() - toolStartedAt,
1299
- status: 'ok',
1300
- });
1301
- return {
1302
- content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
1303
- };
1304
- }
1305
- catch (error) {
1306
- const message = error instanceof Error ? error.message : 'Unknown error';
1307
- const agentHash = creds ? hashAgentId(creds.agentId) : undefined;
1308
- emitTelemetry({
1309
- event: 'tool.call',
1310
- tool: name,
1311
- agentIdHash: agentHash,
1312
- latencyMs: Date.now() - toolStartedAt,
1313
- status: 'error',
1314
- error: message,
1315
- });
1316
- // BondPendingError carries human-readable text that the agent should
1317
- // render verbatim — return as plain text rather than JSON.
1318
- if (error instanceof BondPendingError) {
1319
- return {
1320
- content: [{ type: 'text', text: message }],
1321
- isError: true,
1322
- };
1323
- }
1324
- return {
1325
- content: [{ type: 'text', text: JSON.stringify({ error: message }) }],
1326
- isError: true,
1327
- };
1328
- }
1329
- });
1034
+ server.setRequestHandler(CallToolRequestSchema, createOAuthCallToolHandler({
1035
+ aisUrl: AIS_URL,
1036
+ server,
1037
+ activityTracker,
1038
+ getHarnessIdentity: () => harnessIdentity,
1039
+ getCredentials: () => creds,
1040
+ setCredentials: (next) => { creds = next; },
1041
+ setSessionAgentOverride: (next) => { sessionAgentOverride = next; },
1042
+ getAgentContext: () => agentContext,
1043
+ setAgentContext: (next) => { agentContext = next; },
1044
+ ensureCredentials: () => ensureCredentials(),
1045
+ invalidateToken: () => { tokenManager.invalidate({ resetBackoff: true }); },
1046
+ applyMemoryRefresh,
1047
+ resolveAgentForLoad,
1048
+ persistClaudeSessionAgentName,
1049
+ isBondPendingError: (error) => error instanceof BondPendingError,
1050
+ stderr,
1051
+ }));
1330
1052
  // Connect the transport BEFORE any auth so MCP `initialize` returns in <1s.
1331
1053
  const transport = new StdioServerTransport();
1332
1054
  await server.connect(transport);
@@ -1349,6 +1071,9 @@ async function main() {
1349
1071
  else if (creds) {
1350
1072
  stderr('Device key enrolled — deferring session setup to the first key-auth token acquire.');
1351
1073
  }
1074
+ else if (hasEnrolledKey()) {
1075
+ stderr('Using enrolled device key; session starts on first tool call.');
1076
+ }
1352
1077
  else {
1353
1078
  stderr(`${MISSING_CREDENTIAL_HELP} Bond URL will be surfaced on first tool call.`);
1354
1079
  }