nolo-cli 0.1.41 → 0.1.43

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 (136) hide show
  1. package/README.md +27 -12
  2. package/agent-runtime/agentThread.ts +222 -0
  3. package/agent-runtime/agentThreadAdmission.ts +74 -0
  4. package/agent-runtime/dialogWritePlan.ts +42 -0
  5. package/agent-runtime/hostAdapter.ts +1 -0
  6. package/agent-runtime/index.ts +43 -0
  7. package/agent-runtime/localLoop.ts +31 -2
  8. package/agent-runtime/localToolPolicy.ts +1 -0
  9. package/agent-runtime/localWorkspaceTools.ts +1258 -78
  10. package/agent-runtime/noloWorkspaceTools.ts +20 -1
  11. package/agent-runtime/runtimeToolPolicy.ts +1 -5
  12. package/agent-runtime/runtimeToolSurface.ts +1 -0
  13. package/agentAliases.ts +29 -9
  14. package/agentInternalCommandEntries.ts +11 -3
  15. package/agentMachineCommands.ts +1 -0
  16. package/agentRecordCommands.ts +2 -2
  17. package/agentRecordHelpers.ts +59 -6
  18. package/agentRunCommand.ts +84 -19
  19. package/ai/agent/buildSystemPrompt.ts +14 -0
  20. package/ai/agent/cliExecutor.ts +202 -43
  21. package/ai/agent/createAgentSchema.ts +2 -2
  22. package/ai/agent/liveAudioModel.ts +1 -1
  23. package/ai/agent/streamAgentChatTurn.ts +154 -20
  24. package/ai/agent/streamAgentChatTurnUtils.ts +63 -0
  25. package/ai/ai.locale.ts +70 -0
  26. package/ai/chat/inlineImageUrlsForCustomProvider.ts +7 -2
  27. package/ai/chat/sendOpenAICompletionsRequest.ts +25 -1
  28. package/ai/llm/kimi.ts +0 -1
  29. package/ai/llm/openrouterModels.ts +27 -2
  30. package/ai/memory/policy.ts +49 -0
  31. package/ai/memory/queryShared.ts +10 -4
  32. package/ai/memory/recentRelationshipRecap.ts +4 -0
  33. package/ai/memory/remember.ts +48 -104
  34. package/ai/memory/scope.ts +74 -0
  35. package/ai/memory/types.ts +6 -0
  36. package/ai/memory/understanding.ts +10 -18
  37. package/ai/policy/runtimePolicy.ts +95 -23
  38. package/ai/tools/agent/agentTools.ts +0 -21
  39. package/ai/tools/agent/startAgentDialogTool.ts +2 -1
  40. package/ai/tools/index.ts +2 -20
  41. package/ai/tools/prepareTools.ts +171 -1
  42. package/ai/tools/readXhsProfileTool.ts +80 -69
  43. package/ai/tools/table/createTableTool.ts +11 -0
  44. package/ai/tools/toolPacks.ts +19 -0
  45. package/ai/types.ts +3 -0
  46. package/app/favorite/deletedFavoriteProjection.ts +31 -0
  47. package/app/hooks/deleteDbKey.ts +24 -3
  48. package/app/i18n/translations/interface.locale.ts +48 -16
  49. package/app/types/appSummary.ts +4 -0
  50. package/app/utils/desktopAgentRuntimeTurnClient.ts +2 -0
  51. package/auth/adminPermissions.ts +64 -4
  52. package/auth/routes.ts +5 -0
  53. package/chat/chat.locale.ts +4 -0
  54. package/chat/dialog/actions/handleSendMessageAction.ts +7 -2
  55. package/chat/dialog/dialogAttachmentCleanup.ts +135 -0
  56. package/chat/dialog/dialogSlice.ts +117 -4
  57. package/chat/messages/activityCompletion.ts +97 -0
  58. package/chat/messages/imagePayloadPersistence.ts +22 -0
  59. package/chat/messages/messageContent.ts +20 -16
  60. package/chat/messages/messageSlice.ts +20 -1
  61. package/chat/messages/toolThunks.ts +32 -11
  62. package/chat/messages/types.ts +8 -1
  63. package/chat/messages/web/toolDisplayName.ts +686 -0
  64. package/cli/agentAliases.ts +29 -9
  65. package/cli/agentInternalCommandEntries.ts +11 -3
  66. package/cli/agentMachineCommands.ts +1 -0
  67. package/cli/agentRecordCommands.ts +2 -2
  68. package/cli/agentRecordHelpers.ts +59 -6
  69. package/cli/agentRunCommand.ts +84 -19
  70. package/cli/client/agentRun.ts +65 -253
  71. package/cli/client/localRuntimeAdapter.ts +179 -53
  72. package/cli/commandRegistry.ts +7 -4
  73. package/cli/dialogAttachmentCleanup.ts +151 -0
  74. package/cli/dialogCommands.ts +163 -61
  75. package/cli/dialogInternalCommandEntries.ts +1 -1
  76. package/cli/docCreateCommands.ts +345 -0
  77. package/cli/docInternalCommandEntries.ts +11 -0
  78. package/cli/docPageHelpers.ts +80 -0
  79. package/cli/docSkillHelpers.ts +67 -0
  80. package/cli/docSpaceHelpers.ts +46 -0
  81. package/cli/internalCommandEntries.ts +4 -0
  82. package/cli/machineCommands.ts +18 -34
  83. package/cli/memoryCommands.ts +253 -0
  84. package/cli/memoryInternalCommandEntries.ts +9 -0
  85. package/cli/qoderUsageProbe.ts +252 -0
  86. package/cli/scriptCommandEntries.ts +0 -3
  87. package/cli/secretScan.ts +31 -0
  88. package/cli/tableCommands.ts +248 -13
  89. package/cli/workflowInternalCommandEntries.ts +2 -3
  90. package/client/agentRun.test.ts +68 -84
  91. package/client/agentRun.ts +65 -253
  92. package/client/localRuntimeAdapter.test.ts +505 -74
  93. package/client/localRuntimeAdapter.ts +179 -53
  94. package/commandRegistry.ts +7 -4
  95. package/connector-experimental/capabilities.ts +10 -9
  96. package/create/space/sidebarVisibleTypes.ts +19 -0
  97. package/dialogAttachmentCleanup.ts +151 -0
  98. package/dialogCommands.ts +163 -61
  99. package/dialogInternalCommandEntries.ts +1 -1
  100. package/docCreateCommands.ts +345 -0
  101. package/docInternalCommandEntries.ts +11 -0
  102. package/docPageHelpers.ts +80 -0
  103. package/docSkillHelpers.ts +67 -0
  104. package/docSpaceHelpers.ts +46 -0
  105. package/integrations/google/models.ts +4 -4
  106. package/integrations/openai/generateOpenAIRequestBody.ts +0 -3
  107. package/integrations/xhs-reader/types.ts +36 -3
  108. package/integrations/xhs-reader/url.ts +24 -1
  109. package/internalCommandEntries.ts +4 -0
  110. package/machineCommands.ts +18 -34
  111. package/memoryCommands.ts +253 -0
  112. package/memoryInternalCommandEntries.ts +9 -0
  113. package/package.json +141 -114
  114. package/qoderUsageProbe.ts +252 -0
  115. package/render/contentIcon/types.ts +167 -0
  116. package/render/page/docSlice.ts +30 -3
  117. package/render/page/types.ts +2 -0
  118. package/render/table/createTableAction.ts +4 -0
  119. package/render/table/tableSlice.ts +61 -0
  120. package/render/table/types.ts +10 -0
  121. package/scriptCommandEntries.ts +0 -3
  122. package/secretScan.ts +31 -0
  123. package/server/handlers/agentRun/types.ts +24 -23
  124. package/tableCommands.ts +248 -13
  125. package/workflowInternalCommandEntries.ts +2 -3
  126. package/ai/tools/agent/taskRunTool.ts +0 -112
  127. package/cli/client/taskRunPrompt.ts +0 -27
  128. package/cli/taskRunCommand.ts +0 -265
  129. package/client/taskRunPrompt.ts +0 -27
  130. package/integrations/xhs-reader/analyze/profileAnalyzer.ts +0 -205
  131. package/integrations/xhs-reader/backends/playwrightProfileCollector.ts +0 -624
  132. package/integrations/xhs-reader/bridge/readXhsProfileWithBridge.ts +0 -227
  133. package/integrations/xhs-reader/client/xhsWebApiClient.ts +0 -229
  134. package/integrations/xhs-reader/normalize.ts +0 -390
  135. package/integrations/xhs-reader/orchestrator.ts +0 -178
  136. package/taskRunCommand.ts +0 -265
@@ -4,7 +4,7 @@ import { join } from "node:path";
4
4
  import { tmpdir } from "node:os";
5
5
 
6
6
  import { runLocalAgentTurn } from "../agent-runtime/localLoop";
7
- import { MIMO_MONTH_AGENT_KEY } from "../agentAliases";
7
+ import { LOCAL_CODEX_AGENT_KEY, LOCAL_QODER_AGENT_KEY, MIMO_MONTH_AGENT_KEY } from "../agentAliases";
8
8
  import { createCliLocalRuntimeAdapter } from "./localRuntimeAdapter";
9
9
 
10
10
  describe("CLI local runtime adapter", () => {
@@ -13,6 +13,7 @@ describe("CLI local runtime adapter", () => {
13
13
  "readFile",
14
14
  "writeFile",
15
15
  "editFile",
16
+ "globFiles",
16
17
  "searchFiles",
17
18
  "execShell",
18
19
  ];
@@ -21,6 +22,7 @@ describe("CLI local runtime adapter", () => {
21
22
  "readFile",
22
23
  "writeFile",
23
24
  "editFile",
25
+ "globFiles",
24
26
  "searchFiles",
25
27
  "execShell",
26
28
  ];
@@ -36,6 +38,7 @@ describe("CLI local runtime adapter", () => {
36
38
  "readSpace",
37
39
  "readDoc",
38
40
  "readSkillDoc",
41
+ "listTables",
39
42
  "queryTableRows",
40
43
  "cliWhoami",
41
44
  "cliDoctor",
@@ -49,6 +52,87 @@ describe("CLI local runtime adapter", () => {
49
52
  return request?.body?.tools?.map((tool: any) => tool.function.name) ?? [];
50
53
  }
51
54
 
55
+ function publicSchemaKeys(schema: any) {
56
+ return Object.keys(schema.parameters.properties).filter((key) => key !== "_activity");
57
+ }
58
+
59
+ test("loads stored local CLI agent records before falling back to built-ins", async () => {
60
+ const adapter = createCliLocalRuntimeAdapter({
61
+ env: {
62
+ NOLO_LOCAL_USER_ID: "0e95801d90",
63
+ },
64
+ db: {
65
+ get: async (key) => {
66
+ if (key !== LOCAL_CODEX_AGENT_KEY) throw new Error(`not found: ${key}`);
67
+ return {
68
+ dbKey: LOCAL_CODEX_AGENT_KEY,
69
+ id: "01LOCALCODEXCLI000000NEW",
70
+ name: "User Edited Local Codex",
71
+ prompt: "User-owned local Codex prompt.",
72
+ apiSource: "cli",
73
+ provider: "cli",
74
+ cliProvider: "codex",
75
+ model: "gpt-5.4",
76
+ };
77
+ },
78
+ put: async () => {},
79
+ batch: async () => {},
80
+ iterator: () => (async function* () {})(),
81
+ },
82
+ fetchImpl: async () => new Response("not found", { status: 404 }),
83
+ } as any);
84
+
85
+ await expect(adapter.loadAgentConfig(LOCAL_CODEX_AGENT_KEY)).resolves.toMatchObject({
86
+ key: LOCAL_CODEX_AGENT_KEY,
87
+ name: "User Edited Local Codex",
88
+ prompt: "User-owned local Codex prompt.",
89
+ apiSource: "cli",
90
+ provider: "cli",
91
+ cliProvider: "codex",
92
+ model: "gpt-5.4",
93
+ });
94
+ });
95
+
96
+ test("falls back to built-in local Codex and Qoder CLI agents without machine binding", async () => {
97
+ const adapter = createCliLocalRuntimeAdapter({
98
+ env: {
99
+ NOLO_LOCAL_USER_ID: "user-1",
100
+ },
101
+ db: {
102
+ get: async (key) => {
103
+ throw new Error(`not found: ${key}`);
104
+ },
105
+ put: async () => {},
106
+ batch: async () => {},
107
+ iterator: () => (async function* () {})(),
108
+ },
109
+ fetchImpl: async () => new Response("not found", { status: 404 }),
110
+ } as any);
111
+
112
+ const codex = await adapter.loadAgentConfig(LOCAL_CODEX_AGENT_KEY);
113
+ const qoder = await adapter.loadAgentConfig(LOCAL_QODER_AGENT_KEY);
114
+
115
+ expect(codex).toMatchObject({
116
+ key: LOCAL_CODEX_AGENT_KEY,
117
+ name: "Local Codex",
118
+ apiSource: "cli",
119
+ provider: "cli",
120
+ cliProvider: "codex",
121
+ });
122
+ expect(qoder).toMatchObject({
123
+ key: LOCAL_QODER_AGENT_KEY,
124
+ name: "Local Qoder",
125
+ apiSource: "cli",
126
+ provider: "cli",
127
+ cliProvider: "qoder",
128
+ model: "Qwen3.7-Max",
129
+ });
130
+ expect((codex as any)?.runtimeBinding).toBeUndefined();
131
+ expect((qoder as any)?.runtimeBinding).toBeUndefined();
132
+ expect((codex as any)?.rawRecord?.runtimeBinding).toBeUndefined();
133
+ expect((qoder as any)?.rawRecord?.runtimeBinding).toBeUndefined();
134
+ });
135
+
52
136
  test("runs cli-provider agents through the local CLI executor instead of OpenAI-compatible direct mode", async () => {
53
137
  const cliExecutions: Array<{ provider: string; prompt: string; options: any }> = [];
54
138
  const adapter = createCliLocalRuntimeAdapter({
@@ -1324,7 +1408,6 @@ describe("CLI local runtime adapter", () => {
1324
1408
  "execShell",
1325
1409
  "checkEnv",
1326
1410
  "queryTableRows",
1327
- "taskRun",
1328
1411
  ],
1329
1412
  }),
1330
1413
  put: async () => {},
@@ -1348,52 +1431,58 @@ describe("CLI local runtime adapter", () => {
1348
1431
  expect(toolNamesFromRequest(requests[0])).toEqual(SHELL_LOCAL_CODING_TOOL_NAMES);
1349
1432
  });
1350
1433
 
1351
- test("allows default semantic workspace tools without legacy agent tool declarations", async () => {
1352
- const workspaceRoot = mkdtempSync(join(tmpdir(), "nolo-cli-runtime-"));
1353
- try {
1354
- await Bun.write(join(workspaceRoot, "README.md"), "local ok\n");
1355
- const adapter = createCliLocalRuntimeAdapter({
1356
- env: {},
1357
- cwd: workspaceRoot,
1358
- db: {
1359
- get: async () => ({
1360
- dbKey: "agent-local-default-tools",
1361
- prompt: "Use local workspace tools.",
1362
- model: "gpt-4.1-mini",
1363
- }),
1364
- put: async () => {},
1365
- batch: async () => {},
1366
- iterator: () => (async function* () {})(),
1367
- },
1368
- fetchImpl: async () => Response.json({}),
1369
- });
1434
+ test("can expose only declared local workspace tools for tool ablations", async () => {
1435
+ const requests: Array<{ body: any }> = [];
1436
+ const adapter = createCliLocalRuntimeAdapter({
1437
+ env: {
1438
+ OPENAI_API_KEY: "sk-local",
1439
+ NOLO_LOCAL_OPENAI_BASE_URL: "http://127.0.0.1:11434/v1",
1440
+ NOLO_LOCAL_WORKSPACE_TOOLSET: "declared-only",
1441
+ },
1442
+ db: {
1443
+ get: async () => ({
1444
+ dbKey: MIMO_MONTH_AGENT_KEY,
1445
+ prompt: "Use exactly the declared local tools.",
1446
+ model: "mimo-v2.5-pro",
1447
+ provider: "custom",
1448
+ tools: ["listFiles", "readFile", "execShell"],
1449
+ }),
1450
+ put: async () => {},
1451
+ batch: async () => {},
1452
+ iterator: () => (async function* () {})(),
1453
+ },
1454
+ fetchImpl: async (_url, init) => {
1455
+ requests.push({ body: JSON.parse(String(init?.body)) });
1456
+ return Response.json({
1457
+ choices: [{ message: { content: "done" } }],
1458
+ });
1459
+ },
1460
+ });
1370
1461
 
1371
- await adapter.loadAgentConfig("default-tools");
1372
- await expect(adapter.executeTool({
1373
- id: "call-read",
1374
- name: "readFile",
1375
- arguments: JSON.stringify({ path: "README.md" }),
1376
- })).resolves.toMatchObject({
1377
- content: "local ok\n",
1378
- });
1379
- } finally {
1380
- rmSync(workspaceRoot, { recursive: true, force: true });
1381
- }
1462
+ await runLocalAgentTurn({
1463
+ adapter,
1464
+ agentRef: MIMO_MONTH_AGENT_KEY,
1465
+ input: "inspect cwd",
1466
+ });
1467
+
1468
+ expect(toolNamesFromRequest(requests[0])).toEqual(["listFiles", "readFile", "execShell"]);
1382
1469
  });
1383
1470
 
1384
- test("exposes declared taskRun as a platform tool in local runtime requests", async () => {
1471
+ test("defaults local workspace tools to strategy descriptions and rich parameters", async () => {
1385
1472
  const requests: Array<{ body: any }> = [];
1386
1473
  const adapter = createCliLocalRuntimeAdapter({
1387
1474
  env: {
1388
1475
  OPENAI_API_KEY: "sk-local",
1389
1476
  NOLO_LOCAL_OPENAI_BASE_URL: "http://127.0.0.1:11434/v1",
1477
+ NOLO_LOCAL_WORKSPACE_TOOLSET: "declared-only",
1390
1478
  },
1391
1479
  db: {
1392
1480
  get: async () => ({
1393
- dbKey: "agent-local-taskrun",
1394
- prompt: "Use taskRun.",
1395
- model: "gpt-4.1-mini",
1396
- toolNames: ["taskRun"],
1481
+ dbKey: MIMO_MONTH_AGENT_KEY,
1482
+ prompt: "Use local coding tools.",
1483
+ model: "mimo-v2.5-pro",
1484
+ provider: "custom",
1485
+ tools: ["listFiles", "readFile", "globFiles", "searchFiles"],
1397
1486
  }),
1398
1487
  put: async () => {},
1399
1488
  batch: async () => {},
@@ -1409,61 +1498,272 @@ describe("CLI local runtime adapter", () => {
1409
1498
 
1410
1499
  await runLocalAgentTurn({
1411
1500
  adapter,
1412
- agentRef: "taskrun",
1413
- input: "read task context",
1501
+ agentRef: MIMO_MONTH_AGENT_KEY,
1502
+ input: "inspect cwd",
1414
1503
  });
1415
1504
 
1416
- expect(toolNamesFromRequest(requests[0])).toContain("taskRun");
1505
+ const tools = new Map(
1506
+ requests[0]?.body.tools.map((tool: any) => [tool.function.name, tool.function]),
1507
+ );
1508
+ const listFiles = tools.get("listFiles") as any;
1509
+ const readFile = tools.get("readFile") as any;
1510
+ const globFiles = tools.get("globFiles") as any;
1511
+ const searchFiles = tools.get("searchFiles") as any;
1512
+
1513
+ expect(listFiles.description).toContain("not for path-pattern discovery across the repo");
1514
+ expect(publicSchemaKeys(listFiles)).toEqual([
1515
+ "path",
1516
+ "maxDepth",
1517
+ "maxResults",
1518
+ "entryType",
1519
+ ]);
1520
+ expect(readFile.description).toContain("Use line ranges");
1521
+ expect(publicSchemaKeys(readFile)).toEqual([
1522
+ "path",
1523
+ "startLine",
1524
+ "endLine",
1525
+ "maxLines",
1526
+ "tailLines",
1527
+ ]);
1528
+ expect(globFiles.description).toContain("any file discovery task");
1529
+ expect(publicSchemaKeys(globFiles)).toEqual([
1530
+ "pattern",
1531
+ "path",
1532
+ "exclude",
1533
+ "includeIgnored",
1534
+ "maxResults",
1535
+ ]);
1536
+ expect(searchFiles.description).toContain("using ripgrep when available");
1537
+ expect(publicSchemaKeys(searchFiles)).toEqual([
1538
+ "query",
1539
+ "path",
1540
+ "exclude",
1541
+ "includeIgnored",
1542
+ "maxResults",
1543
+ "literal",
1544
+ "caseSensitive",
1545
+ "contextLines",
1546
+ ]);
1417
1547
  });
1418
1548
 
1419
- test("executes declared taskRun through the server task-run bridge", async () => {
1420
- const requests: Array<{ url: string; auth: string | null; body: any }> = [];
1549
+ test("can vary globFiles schema through local runtime env", async () => {
1550
+ const requests: Array<{ body: any }> = [];
1421
1551
  const adapter = createCliLocalRuntimeAdapter({
1422
1552
  env: {
1423
- BASE_URL: "https://us.nolo.chat",
1424
- AUTH_TOKEN: "machine-token",
1553
+ OPENAI_API_KEY: "sk-local",
1554
+ NOLO_LOCAL_OPENAI_BASE_URL: "http://127.0.0.1:11434/v1",
1555
+ NOLO_GLOBFILES_DESCRIPTION_VARIANT: "antiShell",
1556
+ NOLO_GLOBFILES_PARAMETER_VARIANT: "rich",
1425
1557
  },
1426
1558
  db: {
1427
1559
  get: async () => ({
1428
- dbKey: "agent-local-taskrun",
1429
- toolNames: ["taskRun"],
1560
+ dbKey: MIMO_MONTH_AGENT_KEY,
1561
+ prompt: "Use local coding tools.",
1562
+ model: "mimo-v2.5-pro",
1563
+ provider: "custom",
1564
+ tools: ["globFiles"],
1430
1565
  }),
1431
1566
  put: async () => {},
1432
1567
  batch: async () => {},
1433
1568
  iterator: () => (async function* () {})(),
1434
1569
  },
1435
- fetchImpl: async (url, init) => {
1436
- requests.push({
1437
- url: String(url),
1438
- auth: new Headers(init?.headers).get("authorization"),
1439
- body: JSON.parse(String(init?.body)),
1570
+ fetchImpl: async (_url, init) => {
1571
+ requests.push({ body: JSON.parse(String(init?.body)) });
1572
+ return Response.json({
1573
+ choices: [{ message: { content: "done" } }],
1440
1574
  });
1441
- return Response.json({ ok: true, row: { rowId: "task-1" } });
1442
1575
  },
1443
1576
  });
1444
1577
 
1445
- await adapter.loadAgentConfig("taskrun");
1446
- const result = await adapter.executeTool({
1447
- id: "call-taskrun",
1448
- name: "taskRun",
1449
- arguments: JSON.stringify({
1450
- action: "readTaskContext",
1451
- dbKey: "row-user-NOLOTASKBOARD-task-1",
1452
- }),
1578
+ await runLocalAgentTurn({
1579
+ adapter,
1580
+ agentRef: MIMO_MONTH_AGENT_KEY,
1581
+ input: "find tests",
1453
1582
  });
1454
1583
 
1455
- expect(requests).toEqual([
1456
- {
1457
- url: "https://us.nolo.chat/api/task-run",
1458
- auth: "Bearer machine-token",
1459
- body: {
1460
- action: "readTaskContext",
1461
- dbKey: "row-user-NOLOTASKBOARD-task-1",
1462
- },
1584
+ const globFiles = requests[0]?.body.tools.find((tool: any) => tool.function.name === "globFiles")?.function;
1585
+ expect(globFiles.description).toContain("Do not use execShell or listFiles for path discovery");
1586
+ expect(publicSchemaKeys(globFiles)).toEqual([
1587
+ "pattern",
1588
+ "path",
1589
+ "exclude",
1590
+ "includeIgnored",
1591
+ "maxResults",
1592
+ ]);
1593
+ });
1594
+
1595
+ test("can vary listFiles schema through local runtime env", async () => {
1596
+ const requests: Array<{ body: any }> = [];
1597
+ const adapter = createCliLocalRuntimeAdapter({
1598
+ env: {
1599
+ OPENAI_API_KEY: "sk-local",
1600
+ NOLO_LOCAL_OPENAI_BASE_URL: "http://127.0.0.1:11434/v1",
1601
+ NOLO_LISTFILES_DESCRIPTION_VARIANT: "antiShell",
1602
+ NOLO_LISTFILES_PARAMETER_VARIANT: "rich",
1603
+ },
1604
+ db: {
1605
+ get: async () => ({
1606
+ dbKey: MIMO_MONTH_AGENT_KEY,
1607
+ prompt: "Use local coding tools.",
1608
+ model: "mimo-v2.5-pro",
1609
+ provider: "custom",
1610
+ tools: ["listFiles"],
1611
+ }),
1612
+ put: async () => {},
1613
+ batch: async () => {},
1614
+ iterator: () => (async function* () {})(),
1615
+ },
1616
+ fetchImpl: async (_url, init) => {
1617
+ requests.push({ body: JSON.parse(String(init?.body)) });
1618
+ return Response.json({
1619
+ choices: [{ message: { content: "done" } }],
1620
+ });
1463
1621
  },
1622
+ });
1623
+
1624
+ await runLocalAgentTurn({
1625
+ adapter,
1626
+ agentRef: MIMO_MONTH_AGENT_KEY,
1627
+ input: "list directories",
1628
+ });
1629
+
1630
+ const listFiles = requests[0]?.body.tools.find((tool: any) => tool.function.name === "listFiles")?.function;
1631
+ expect(listFiles.description).toContain("Do not use execShell with ls/find/tree");
1632
+ expect(publicSchemaKeys(listFiles)).toEqual([
1633
+ "path",
1634
+ "maxDepth",
1635
+ "maxResults",
1636
+ "entryType",
1464
1637
  ]);
1465
- expect(result.content).toContain("\"ok\":true");
1466
- expect(result.metadata).toMatchObject({ serverPlatformTool: true });
1638
+ });
1639
+
1640
+ test("can vary readFile schema through local runtime env", async () => {
1641
+ const requests: Array<{ body: any }> = [];
1642
+ const adapter = createCliLocalRuntimeAdapter({
1643
+ env: {
1644
+ OPENAI_API_KEY: "sk-local",
1645
+ NOLO_LOCAL_OPENAI_BASE_URL: "http://127.0.0.1:11434/v1",
1646
+ NOLO_READFILE_DESCRIPTION_VARIANT: "strategy",
1647
+ NOLO_READFILE_PARAMETER_VARIANT: "rich",
1648
+ },
1649
+ db: {
1650
+ get: async () => ({
1651
+ dbKey: MIMO_MONTH_AGENT_KEY,
1652
+ prompt: "Use local coding tools.",
1653
+ model: "mimo-v2.5-pro",
1654
+ provider: "custom",
1655
+ tools: ["readFile"],
1656
+ }),
1657
+ put: async () => {},
1658
+ batch: async () => {},
1659
+ iterator: () => (async function* () {})(),
1660
+ },
1661
+ fetchImpl: async (_url, init) => {
1662
+ requests.push({ body: JSON.parse(String(init?.body)) });
1663
+ return Response.json({
1664
+ choices: [{ message: { content: "done" } }],
1665
+ });
1666
+ },
1667
+ });
1668
+
1669
+ await runLocalAgentTurn({
1670
+ adapter,
1671
+ agentRef: MIMO_MONTH_AGENT_KEY,
1672
+ input: "read a file range",
1673
+ });
1674
+
1675
+ const readFile = requests[0]?.body.tools.find((tool: any) => tool.function.name === "readFile")?.function;
1676
+ expect(readFile.description).toContain("Use line ranges");
1677
+ expect(readFile.parameters.properties.maxLines.description).toContain("each readFile preview consumes one budget slot");
1678
+ expect(publicSchemaKeys(readFile)).toEqual([
1679
+ "path",
1680
+ "startLine",
1681
+ "endLine",
1682
+ "maxLines",
1683
+ "tailLines",
1684
+ ]);
1685
+ });
1686
+
1687
+ test("can vary searchFiles schema through local runtime env", async () => {
1688
+ const requests: Array<{ body: any }> = [];
1689
+ const adapter = createCliLocalRuntimeAdapter({
1690
+ env: {
1691
+ OPENAI_API_KEY: "sk-local",
1692
+ NOLO_LOCAL_OPENAI_BASE_URL: "http://127.0.0.1:11434/v1",
1693
+ NOLO_SEARCHFILES_DESCRIPTION_VARIANT: "antiShell",
1694
+ NOLO_SEARCHFILES_PARAMETER_VARIANT: "rich",
1695
+ },
1696
+ db: {
1697
+ get: async () => ({
1698
+ dbKey: MIMO_MONTH_AGENT_KEY,
1699
+ prompt: "Use local coding tools.",
1700
+ model: "mimo-v2.5-pro",
1701
+ provider: "custom",
1702
+ tools: ["searchFiles"],
1703
+ }),
1704
+ put: async () => {},
1705
+ batch: async () => {},
1706
+ iterator: () => (async function* () {})(),
1707
+ },
1708
+ fetchImpl: async (_url, init) => {
1709
+ requests.push({ body: JSON.parse(String(init?.body)) });
1710
+ return Response.json({
1711
+ choices: [{ message: { content: "done" } }],
1712
+ });
1713
+ },
1714
+ });
1715
+
1716
+ await runLocalAgentTurn({
1717
+ adapter,
1718
+ agentRef: MIMO_MONTH_AGENT_KEY,
1719
+ input: "find TODO",
1720
+ });
1721
+
1722
+ const searchFiles = requests[0]?.body.tools.find((tool: any) => tool.function.name === "searchFiles")?.function;
1723
+ expect(searchFiles.description).toContain("Do not use execShell for content search");
1724
+ expect(publicSchemaKeys(searchFiles)).toEqual([
1725
+ "query",
1726
+ "path",
1727
+ "exclude",
1728
+ "includeIgnored",
1729
+ "maxResults",
1730
+ "literal",
1731
+ "caseSensitive",
1732
+ "contextLines",
1733
+ ]);
1734
+ });
1735
+
1736
+ test("allows default semantic workspace tools without legacy agent tool declarations", async () => {
1737
+ const workspaceRoot = mkdtempSync(join(tmpdir(), "nolo-cli-runtime-"));
1738
+ try {
1739
+ await Bun.write(join(workspaceRoot, "README.md"), "local ok\n");
1740
+ const adapter = createCliLocalRuntimeAdapter({
1741
+ env: {},
1742
+ cwd: workspaceRoot,
1743
+ db: {
1744
+ get: async () => ({
1745
+ dbKey: "agent-local-default-tools",
1746
+ prompt: "Use local workspace tools.",
1747
+ model: "gpt-4.1-mini",
1748
+ }),
1749
+ put: async () => {},
1750
+ batch: async () => {},
1751
+ iterator: () => (async function* () {})(),
1752
+ },
1753
+ fetchImpl: async () => Response.json({}),
1754
+ });
1755
+
1756
+ await adapter.loadAgentConfig("default-tools");
1757
+ await expect(adapter.executeTool({
1758
+ id: "call-read",
1759
+ name: "readFile",
1760
+ arguments: JSON.stringify({ path: "README.md" }),
1761
+ })).resolves.toMatchObject({
1762
+ content: "local ok\n",
1763
+ });
1764
+ } finally {
1765
+ rmSync(workspaceRoot, { recursive: true, force: true });
1766
+ }
1467
1767
  });
1468
1768
 
1469
1769
  test("exposes declared server table write tools in local runtime requests", async () => {
@@ -1541,6 +1841,7 @@ describe("CLI local runtime adapter", () => {
1541
1841
  userInput: "采集这个用户并写入 table",
1542
1842
  arguments: JSON.stringify({
1543
1843
  title: "XHS profile",
1844
+ purpose: "agent_eval_workbench",
1544
1845
  columns: [{ name: "nickname" }],
1545
1846
  }),
1546
1847
  });
@@ -1551,6 +1852,7 @@ describe("CLI local runtime adapter", () => {
1551
1852
  auth: "Bearer token-1",
1552
1853
  body: {
1553
1854
  title: "XHS profile",
1855
+ purpose: "agent_eval_workbench",
1554
1856
  columns: [{ name: "nickname" }],
1555
1857
  },
1556
1858
  },
@@ -2063,6 +2365,76 @@ describe("CLI local runtime adapter", () => {
2063
2365
  expect(toolNamesFromRequest(requests[0])).toContain("read_xhs_profile");
2064
2366
  });
2065
2367
 
2368
+ test("exposes read_x_post to OpenAI-compatible providers when the agent declares it", async () => {
2369
+ const requests: Array<{ body: any }> = [];
2370
+ const adapter = createCliLocalRuntimeAdapter({
2371
+ env: {
2372
+ OPENAI_API_KEY: "sk-local",
2373
+ NOLO_LOCAL_OPENAI_BASE_URL: "http://127.0.0.1:11434/v1",
2374
+ },
2375
+ db: {
2376
+ get: async () => ({
2377
+ dbKey: "agent-local-x-post",
2378
+ prompt: "Read X posts.",
2379
+ model: "gpt-4.1-mini",
2380
+ toolNames: ["read_x_post"],
2381
+ }),
2382
+ put: async () => {},
2383
+ batch: async () => {},
2384
+ iterator: () => (async function* () {})(),
2385
+ },
2386
+ fetchImpl: async (_url, init) => {
2387
+ requests.push({ body: JSON.parse(String(init?.body)) });
2388
+ return Response.json({
2389
+ choices: [{ message: { content: "done" } }],
2390
+ });
2391
+ },
2392
+ });
2393
+
2394
+ await runLocalAgentTurn({
2395
+ adapter,
2396
+ agentRef: "x-post",
2397
+ input: "读取 X 帖子",
2398
+ });
2399
+
2400
+ expect(toolNamesFromRequest(requests[0])).toContain("read_x_post");
2401
+ });
2402
+
2403
+ test("adds read_x_post to local web-capable agent tool surface", async () => {
2404
+ const requests: Array<{ body: any }> = [];
2405
+ const adapter = createCliLocalRuntimeAdapter({
2406
+ env: {
2407
+ OPENAI_API_KEY: "sk-local",
2408
+ NOLO_LOCAL_OPENAI_BASE_URL: "http://127.0.0.1:11434/v1",
2409
+ },
2410
+ db: {
2411
+ get: async () => ({
2412
+ dbKey: "agent-local-web-reader",
2413
+ prompt: "Read web pages.",
2414
+ model: "gpt-4.1-mini",
2415
+ toolNames: ["fetchWebpage"],
2416
+ }),
2417
+ put: async () => {},
2418
+ batch: async () => {},
2419
+ iterator: () => (async function* () {})(),
2420
+ },
2421
+ fetchImpl: async (_url, init) => {
2422
+ requests.push({ body: JSON.parse(String(init?.body)) });
2423
+ return Response.json({
2424
+ choices: [{ message: { content: "done" } }],
2425
+ });
2426
+ },
2427
+ });
2428
+
2429
+ await runLocalAgentTurn({
2430
+ adapter,
2431
+ agentRef: "web-reader",
2432
+ input: "读取 X 帖子",
2433
+ });
2434
+
2435
+ expect(toolNamesFromRequest(requests[0])).toContain("read_x_post");
2436
+ });
2437
+
2066
2438
  test("does not expose read_xhs_profile when the agent does not declare it", async () => {
2067
2439
  const requests: Array<{ body: any }> = [];
2068
2440
  const adapter = createCliLocalRuntimeAdapter({
@@ -2159,6 +2531,67 @@ describe("CLI local runtime adapter", () => {
2159
2531
  expect(parsed.data.profile.nickname).toBe("test-soul");
2160
2532
  });
2161
2533
 
2534
+ test("default read_x_post executor calls the local bridge reader", async () => {
2535
+ const readerCalls: Array<{ args: any }> = [];
2536
+ const adapter = createCliLocalRuntimeAdapter({
2537
+ env: {
2538
+ NOLO_LOCAL_USER_ID: "user-1",
2539
+ },
2540
+ db: {
2541
+ get: async () => ({
2542
+ dbKey: "agent-user-1-x-post-default",
2543
+ id: "x-post-default",
2544
+ prompt: "Read X posts.",
2545
+ toolNames: ["read_x_post"],
2546
+ }),
2547
+ put: async () => {},
2548
+ batch: async () => {},
2549
+ iterator: () => (async function* () {})(),
2550
+ },
2551
+ readXPost: async (args) => {
2552
+ readerCalls.push({ args });
2553
+ return {
2554
+ rawData: {
2555
+ ok: true,
2556
+ backend: "desktop_local_browser",
2557
+ fetchedAt: "2026-06-08T00:00:00.000Z",
2558
+ data: {
2559
+ id: "2063842222307704944",
2560
+ url: "https://x.com/BohuTANG/status/2063842222307704944",
2561
+ text: "mock x post",
2562
+ author: {
2563
+ handle: "BohuTANG",
2564
+ displayName: "Bohu",
2565
+ },
2566
+ },
2567
+ } as any,
2568
+ displayData: "已读取 X 帖子:@BohuTANG",
2569
+ };
2570
+ },
2571
+ fetchImpl: async () => Response.json({}),
2572
+ });
2573
+
2574
+ await adapter.loadAgentConfig("x-post-default");
2575
+ const result = await adapter.executeTool({
2576
+ id: "call-x-post-default",
2577
+ name: "read_x_post",
2578
+ arguments: JSON.stringify({
2579
+ url: "https://x.com/BohuTANG/status/2063842222307704944",
2580
+ keepOpen: true,
2581
+ }),
2582
+ });
2583
+
2584
+ expect(readerCalls).toHaveLength(1);
2585
+ expect(readerCalls[0].args).toMatchObject({
2586
+ url: "https://x.com/BohuTANG/status/2063842222307704944",
2587
+ keepOpen: true,
2588
+ });
2589
+ expect(result.metadata).toMatchObject({ xPostLocalBridge: true });
2590
+ const parsed = JSON.parse(result.content);
2591
+ expect(parsed.ok).toBe(true);
2592
+ expect(parsed.data.text).toBe("mock x post");
2593
+ });
2594
+
2162
2595
  test("default read_xhs_profile executor calls the local bridge reader", async () => {
2163
2596
  const readerCalls: Array<{ args: any; thunkApi: any }> = [];
2164
2597
  const adapter = createCliLocalRuntimeAdapter({
@@ -2221,9 +2654,7 @@ describe("CLI local runtime adapter", () => {
2221
2654
  includeComments: true,
2222
2655
  maxCommentPagesPerNote: 1,
2223
2656
  });
2224
- expect(String(readerCalls[0].args.profileDir).replaceAll("\\", "/")).toContain(
2225
- "chat.nolo.desktop/stable/external-readers/xhs-profile"
2226
- );
2657
+ expect(readerCalls[0].args.profileDir).toBeUndefined();
2227
2658
  expect(readerCalls[0].thunkApi).toBeUndefined();
2228
2659
  expect(result.metadata).toMatchObject({
2229
2660
  xhsLocalBridge: true,
@@ -2232,7 +2663,7 @@ describe("CLI local runtime adapter", () => {
2232
2663
  expect(JSON.parse(result.content).data.profile.nickname).toBe("default-bridge");
2233
2664
  });
2234
2665
 
2235
- test("default read_xhs_profile executor honors explicit XHS profile env", async () => {
2666
+ test("default read_xhs_profile executor ignores explicit XHS profile env", async () => {
2236
2667
  const readerCalls: Array<{ args: any }> = [];
2237
2668
  const adapter = createCliLocalRuntimeAdapter({
2238
2669
  env: {