nolo-cli 0.1.18 → 0.1.20

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 (111) hide show
  1. package/README.md +9 -1
  2. package/agent-runtime/agentConfigOptions.ts +12 -0
  3. package/agent-runtime/agentRecordConfig.ts +99 -0
  4. package/agent-runtime/agentRecordKeys.ts +14 -0
  5. package/agent-runtime/dialogMessageRecord.ts +16 -0
  6. package/agent-runtime/dialogWritePlan.ts +130 -0
  7. package/agent-runtime/hostAdapter.ts +14 -0
  8. package/agent-runtime/hybridRecordStore.ts +147 -0
  9. package/agent-runtime/index.ts +69 -0
  10. package/agent-runtime/localLoop.ts +78 -6
  11. package/agent-runtime/localToolPolicy.ts +130 -0
  12. package/agent-runtime/localWorkspaceTools.ts +1532 -0
  13. package/agent-runtime/openAiCompatibleProvider.ts +70 -0
  14. package/agent-runtime/openAiCompatibleProviderConfig.ts +38 -0
  15. package/agent-runtime/platformChatProvider.ts +241 -0
  16. package/agent-runtime/taskWorkspace.ts +193 -0
  17. package/agent-runtime/types.ts +2 -0
  18. package/agent-runtime/workspaceSession.ts +76 -0
  19. package/agentAliases.ts +37 -0
  20. package/agentPullCommand.ts +1 -1
  21. package/agentRunCommand.ts +289 -54
  22. package/agentRuntimeCommands.ts +354 -164
  23. package/agentRuntimeLocal.ts +38 -0
  24. package/ai/agent/agentSlice.ts +10 -0
  25. package/ai/agent/buildEditingContext.ts +5 -0
  26. package/ai/agent/buildSystemPrompt.ts +41 -18
  27. package/ai/agent/canvasEditingContext.ts +49 -0
  28. package/ai/agent/cliExecutor.ts +15 -4
  29. package/ai/agent/createAgentSchema.ts +2 -0
  30. package/ai/agent/executeToolCall.ts +3 -2
  31. package/ai/agent/hooks/usePublicAgents.ts +6 -0
  32. package/ai/agent/pageBuilderHandoffRules.ts +75 -0
  33. package/ai/agent/runAgentClientLoop.ts +4 -1
  34. package/ai/agent/runtimeGuidance.ts +19 -0
  35. package/ai/agent/server/fetchPublicAgents.ts +51 -1
  36. package/ai/agent/streamAgentChatTurn.ts +20 -2
  37. package/ai/agent/streamAgentChatTurnUtils.ts +60 -16
  38. package/ai/chat/accumulateToolCallChunks.ts +40 -9
  39. package/ai/chat/parseApiError.ts +3 -0
  40. package/ai/chat/sendOpenAICompletionsRequest.native.ts +23 -10
  41. package/ai/chat/sendOpenAICompletionsRequest.ts +13 -1
  42. package/ai/chat/updateTotalUsage.ts +26 -9
  43. package/ai/llm/deepinfra.ts +51 -0
  44. package/ai/llm/getPricing.ts +6 -0
  45. package/ai/llm/kimi.ts +2 -0
  46. package/ai/llm/openrouterModels.ts +0 -135
  47. package/ai/llm/providers.ts +1 -0
  48. package/ai/llm/types.ts +8 -0
  49. package/ai/taskRun/taskRunProtocol.ts +823 -0
  50. package/ai/token/calculatePrice.ts +30 -0
  51. package/ai/token/externalToolCost.ts +49 -29
  52. package/ai/token/prepareTokenUsageData.ts +6 -1
  53. package/ai/token/serverTokenWriter.ts +4 -2
  54. package/ai/tools/agent/agentTools.ts +21 -0
  55. package/ai/tools/agent/presets/appBuilderPreset.ts +7 -0
  56. package/ai/tools/agent/streamParallelAgentsTool.ts +2 -1
  57. package/ai/tools/agent/taskRunTool.ts +112 -0
  58. package/ai/tools/applyEditTool.ts +6 -3
  59. package/ai/tools/applyLineEditsTool.ts +6 -3
  60. package/ai/tools/checkEnvTool.ts +14 -9
  61. package/ai/tools/codeSearchTool.ts +17 -5
  62. package/ai/tools/execBashTool.ts +33 -29
  63. package/ai/tools/fetchWebpageSupport.ts +24 -0
  64. package/ai/tools/fetchWebpageTool.ts +18 -5
  65. package/ai/tools/index.ts +158 -0
  66. package/ai/tools/jdProductScraperTool.ts +821 -0
  67. package/ai/tools/listFilesTool.ts +6 -3
  68. package/ai/tools/localFilesTool.ts +200 -0
  69. package/ai/tools/readFileTool.ts +6 -3
  70. package/ai/tools/searchRepoTool.ts +6 -3
  71. package/ai/tools/table/rowTools.ts +6 -1
  72. package/ai/tools/taobaoTmallProductScraperTool.ts +49 -0
  73. package/ai/tools/toolApiClient.ts +20 -6
  74. package/ai/tools/wereadGatewayTool.ts +152 -0
  75. package/ai/tools/writeFileTool.ts +6 -3
  76. package/client/agentConfigResolver.test.ts +70 -0
  77. package/client/agentConfigResolver.ts +1 -0
  78. package/client/agentRun.test.ts +361 -7
  79. package/client/agentRun.ts +449 -63
  80. package/client/hybridRecordStore.test.ts +115 -0
  81. package/client/hybridRecordStore.ts +41 -0
  82. package/client/localAgentRecords.test.ts +27 -0
  83. package/client/localAgentRecords.ts +7 -0
  84. package/client/localDialogRecords.test.ts +124 -0
  85. package/client/localDialogRecords.ts +30 -0
  86. package/client/localProviderResolver.test.ts +78 -0
  87. package/client/localProviderResolver.ts +1 -0
  88. package/client/localRuntimeAdapter.test.ts +813 -20
  89. package/client/localRuntimeAdapter.ts +279 -232
  90. package/client/localRuntimeDryRun.test.ts +116 -0
  91. package/client/localToolPolicy.ts +8 -81
  92. package/client/taskRunPrompt.ts +26 -0
  93. package/client/taskWorktree.ts +8 -0
  94. package/client/workspaceSession.test.ts +57 -0
  95. package/client/workspaceSession.ts +11 -0
  96. package/commandRegistry.ts +23 -6
  97. package/connectorRunArtifact.ts +121 -0
  98. package/database/actions/write.ts +16 -2
  99. package/database/hooks/useUserData.ts +9 -3
  100. package/database/server/dataHandlers.ts +18 -20
  101. package/database/server/emailRepository.ts +3 -3
  102. package/database/server/patch.ts +18 -10
  103. package/database/server/query.ts +43 -4
  104. package/database/server/read.ts +24 -38
  105. package/database/server/recordIdentity.ts +100 -0
  106. package/database/server/write.ts +21 -25
  107. package/index.ts +70 -33
  108. package/machineCommands.ts +318 -144
  109. package/package.json +4 -1
  110. package/tableCommands.ts +181 -0
  111. package/taskRunCommand.ts +237 -0
@@ -0,0 +1,116 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { mkdtempSync, readFileSync, rmSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { tmpdir } from "node:os";
5
+
6
+ import { runLocalAgentTurn } from "../agent-runtime/localLoop";
7
+ import { buildLocalWorkspaceToolset } from "../agent-runtime/localWorkspaceTools";
8
+ import { createCliLocalRuntimeAdapter } from "./localRuntimeAdapter";
9
+
10
+ describe("CLI local runtime dry run", () => {
11
+ test("lets a declared workspace file tool write a file and save the tool trace", async () => {
12
+ const workspaceRoot = mkdtempSync(join(tmpdir(), "nolo-local-runtime-dry-run-"));
13
+ try {
14
+ const records = new Map<string, any>([
15
+ ["agent-user-1-frontend", {
16
+ dbKey: "agent-user-1-frontend",
17
+ id: "frontend",
18
+ name: "Frontend Implementer",
19
+ prompt: "Use workspace file tools to edit files.",
20
+ model: "qwen-coder",
21
+ tools: [
22
+ { type: "function", function: { name: "writeWorkspaceFile" } },
23
+ ],
24
+ }],
25
+ ]);
26
+ const batchOps: any[] = [];
27
+ let completeCount = 0;
28
+ const adapter = createCliLocalRuntimeAdapter({
29
+ env: {
30
+ NOLO_LOCAL_USER_ID: "user-1",
31
+ NOLO_LOCAL_OPENAI_BASE_URL: "http://127.0.0.1:11434/v1",
32
+ },
33
+ db: {
34
+ get: async (key) => {
35
+ if (!records.has(key)) throw new Error(`not found: ${key}`);
36
+ return records.get(key);
37
+ },
38
+ put: async (key, value) => {
39
+ records.set(key, value);
40
+ },
41
+ batch: async (ops) => {
42
+ batchOps.push(...ops);
43
+ for (const op of ops) {
44
+ if (op.type === "put") records.set(op.key, op.value);
45
+ }
46
+ },
47
+ iterator: () => (async function* () {})(),
48
+ },
49
+ cwd: workspaceRoot,
50
+ now: () => 1710000000000,
51
+ createId: () => "dialog-dry-run",
52
+ fetchImpl: async (_url, init) => {
53
+ const body = JSON.parse(String(init?.body));
54
+ completeCount += 1;
55
+ if (completeCount === 1) {
56
+ expect(body.tools.map((tool: any) => tool.function.name)).toEqual([
57
+ ...buildLocalWorkspaceToolset({}).toolNames,
58
+ ]);
59
+ return Response.json({
60
+ model: "qwen-coder",
61
+ choices: [{
62
+ message: {
63
+ content: "",
64
+ tool_calls: [{
65
+ id: "call-1",
66
+ type: "function",
67
+ function: {
68
+ name: "writeWorkspaceFile",
69
+ arguments: JSON.stringify({
70
+ path: "src/notification.css",
71
+ content: ".notification { border-radius: 8px; }\n",
72
+ }),
73
+ },
74
+ }],
75
+ },
76
+ }],
77
+ });
78
+ }
79
+ expect(body.messages.at(-1)).toMatchObject({
80
+ role: "tool",
81
+ content: "wrote src/notification.css",
82
+ tool_call_id: "call-1",
83
+ });
84
+ return Response.json({
85
+ model: "qwen-coder",
86
+ choices: [{ message: { content: "updated" } }],
87
+ });
88
+ },
89
+ });
90
+
91
+ const result = await runLocalAgentTurn({
92
+ adapter,
93
+ agentRef: "frontend",
94
+ input: "fix notification UI",
95
+ });
96
+
97
+ expect(result).toMatchObject({
98
+ dialogId: "dialog-dry-run",
99
+ content: "updated",
100
+ toolCallCount: 1,
101
+ });
102
+ expect(readFileSync(join(workspaceRoot, "src/notification.css"), "utf8")).toBe(
103
+ ".notification { border-radius: 8px; }\n"
104
+ );
105
+ expect(batchOps.map((op) => op.key)).toEqual([
106
+ "dialog-user-1-dialog-dry-run",
107
+ "dialog-dialog-dry-run-msg-1710000000000-001",
108
+ "dialog-dialog-dry-run-msg-1710000000000-002",
109
+ "dialog-dialog-dry-run-msg-1710000000000-003",
110
+ "dialog-dialog-dry-run-msg-1710000000000-004",
111
+ ]);
112
+ } finally {
113
+ rmSync(workspaceRoot, { recursive: true, force: true });
114
+ }
115
+ });
116
+ });
@@ -1,81 +1,8 @@
1
- import type { AgentRuntimeToolCallInput, AgentRuntimeToolResult } from "../../agent-runtime";
2
-
3
- type EnvLike = Record<string, string | undefined>;
4
-
5
- export type LocalToolPolicyDecision =
6
- | { allowed: true; toolName: string }
7
- | { allowed: false; toolName: string; reason: string };
8
-
9
- const NEVER_LOCAL_TOOLS = new Set([
10
- "deleteSpaces",
11
- "updateAgent",
12
- "updateSelf",
13
- "createAgent",
14
- ]);
15
-
16
- function parseCsv(value: string | undefined) {
17
- return (value ?? "")
18
- .split(",")
19
- .map((item) => item.trim())
20
- .filter(Boolean);
21
- }
22
-
23
- export function resolveLocalToolPolicy(args: {
24
- env: EnvLike;
25
- agentToolNames?: string[];
26
- toolName: string;
27
- }): LocalToolPolicyDecision {
28
- if (args.toolName === "execShell") {
29
- const shellMode = args.env.NOLO_LOCAL_SHELL_MODE || "";
30
- if (["worktree", "dangerous", "1", "true"].includes(shellMode)) {
31
- return { allowed: true, toolName: args.toolName };
32
- }
33
- return {
34
- allowed: false,
35
- toolName: args.toolName,
36
- reason: "execShell requires NOLO_LOCAL_SHELL_MODE=worktree.",
37
- };
38
- }
39
-
40
- if (NEVER_LOCAL_TOOLS.has(args.toolName)) {
41
- return {
42
- allowed: false,
43
- toolName: args.toolName,
44
- reason: `${args.toolName} is blocked by the local CLI safety policy.`,
45
- };
46
- }
47
-
48
- const allowedByEnv = new Set(parseCsv(args.env.NOLO_LOCAL_ALLOWED_TOOLS));
49
- const agentTools = new Set(args.agentToolNames ?? []);
50
- if (allowedByEnv.has(args.toolName) && agentTools.has(args.toolName)) {
51
- return { allowed: true, toolName: args.toolName };
52
- }
53
-
54
- return {
55
- allowed: false,
56
- toolName: args.toolName,
57
- reason:
58
- `${args.toolName} is not enabled for local CLI runs. ` +
59
- "Set NOLO_LOCAL_ALLOWED_TOOLS and make sure the agent declares the tool before using it locally.",
60
- };
61
- }
62
-
63
- export async function executeLocalToolWithPolicy(args: {
64
- env: EnvLike;
65
- agentToolNames?: string[];
66
- call: AgentRuntimeToolCallInput;
67
- executors?: Record<string, (call: AgentRuntimeToolCallInput) => Promise<AgentRuntimeToolResult>>;
68
- }): Promise<AgentRuntimeToolResult> {
69
- const decision = resolveLocalToolPolicy({
70
- env: args.env,
71
- agentToolNames: args.agentToolNames,
72
- toolName: args.call.name,
73
- });
74
- if (!decision.allowed) throw new Error(decision.reason);
75
-
76
- const executor = args.executors?.[args.call.name];
77
- if (!executor) {
78
- throw new Error(`${args.call.name} is allowed by policy but no local executor is registered.`);
79
- }
80
- return executor(args.call);
81
- }
1
+ export {
2
+ executeLocalToolWithPolicy,
3
+ resolveLocalToolPolicy,
4
+ } from "../agentRuntimeLocal";
5
+
6
+ export type {
7
+ LocalToolPolicyDecision,
8
+ } from "../agentRuntimeLocal";
@@ -0,0 +1,26 @@
1
+ export type TaskRunPromptContext = {
2
+ rowDbKey: string;
3
+ taskRunId?: string;
4
+ workItemId?: string;
5
+ artifactIds?: string[];
6
+ };
7
+
8
+ export function prependTaskRunPrompt(message: string, context?: TaskRunPromptContext): string {
9
+ if (!context?.rowDbKey) return message;
10
+ const artifactIds = context.artifactIds?.filter(Boolean) ?? [];
11
+ return [
12
+ "AI-native task-run context:",
13
+ "- This system is AI-native. The current task board is only the present observation and validation use case; different users, companies, and workflows may use different task owners later.",
14
+ "- Treat the task row meta as the durable execution context. Do not infer branch, runner, handoff, review, or blocker state from chat history alone.",
15
+ `- rowDbKey: ${context.rowDbKey}`,
16
+ ...(context.taskRunId ? [`- taskRunId: ${context.taskRunId}`] : []),
17
+ ...(context.workItemId ? [`- workItemId: ${context.workItemId}`] : []),
18
+ ...(artifactIds.length ? [`- artifactIds: ${artifactIds.join(",")}`] : []),
19
+ "- When you create, update, hand off, block, or review work, update the task context with `bun run task:run -- ...`.",
20
+ "- If this turn already includes a server TaskRun preflight snapshot, use that snapshot directly for read-only reporting or planning.",
21
+ "- Keep long reasoning and logs in the dialog; record only resumable references in task row meta.",
22
+ "",
23
+ "User task:",
24
+ message,
25
+ ].join("\n");
26
+ }
@@ -0,0 +1,8 @@
1
+ export {
2
+ ensureWorkspacePackageLinks,
3
+ formatPreparedGitTaskWorkspace as formatPreparedTaskWorktree,
4
+ prepareGitTaskWorkspace as prepareTaskWorktree,
5
+ } from "../agent-runtime/taskWorkspace";
6
+ export type {
7
+ PreparedGitTaskWorkspace as PreparedTaskWorktree,
8
+ } from "../agent-runtime/taskWorkspace";
@@ -0,0 +1,57 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import {
4
+ activateWorkspaceSession,
5
+ createWorkspaceSession,
6
+ formatWorkspaceSessionActivation,
7
+ } from "./workspaceSession";
8
+
9
+ describe("CLI workspace session", () => {
10
+ test("starts from an explicit workspace root without preparing a task worktree", async () => {
11
+ const session = createWorkspaceSession({ cwd: "/repo/current" });
12
+
13
+ const result = await activateWorkspaceSession(session, {
14
+ agentKey: "frontend",
15
+ mode: "task-worktree",
16
+ prepareTaskWorkspace: async () => {
17
+ throw new Error("should not prepare a task worktree for explicit cwd");
18
+ },
19
+ });
20
+
21
+ expect(result.workspaceRoot).toBe("/repo/current");
22
+ expect(result.kind).toBe("current");
23
+ });
24
+
25
+ test("activates a task worktree once when requested by agent policy", async () => {
26
+ const calls: string[] = [];
27
+ let session = createWorkspaceSession({ cwd: undefined });
28
+
29
+ session = await activateWorkspaceSession(session, {
30
+ agentKey: "agent-user-1-frontend",
31
+ mode: "task-worktree",
32
+ prepareTaskWorkspace: async ({ agentKey }) => {
33
+ calls.push(agentKey);
34
+ return {
35
+ path: "/repo/.worktrees/nolo-agent-frontend",
36
+ branchName: "nolo-agent-frontend",
37
+ };
38
+ },
39
+ });
40
+ session = await activateWorkspaceSession(session, {
41
+ agentKey: "agent-user-1-frontend",
42
+ mode: "task-worktree",
43
+ prepareTaskWorkspace: async () => {
44
+ throw new Error("task worktree should only be prepared once");
45
+ },
46
+ });
47
+
48
+ expect(calls).toEqual(["agent-user-1-frontend"]);
49
+ expect(session).toMatchObject({
50
+ kind: "task-worktree",
51
+ workspaceRoot: "/repo/.worktrees/nolo-agent-frontend",
52
+ branchName: "nolo-agent-frontend",
53
+ });
54
+ expect(formatWorkspaceSessionActivation(session)).toContain("workspace session: task-worktree");
55
+ expect(formatWorkspaceSessionActivation(session)).toContain("/repo/.worktrees/nolo-agent-frontend");
56
+ });
57
+ });
@@ -0,0 +1,11 @@
1
+ export {
2
+ activateWorkspaceSession,
3
+ createWorkspaceSession,
4
+ formatWorkspaceSessionActivation,
5
+ } from "../agent-runtime/workspaceSession";
6
+ export type {
7
+ PrepareTaskWorkspace,
8
+ PreparedTaskWorkspace,
9
+ WorkspaceSession,
10
+ WorkspaceSessionMode,
11
+ } from "../agent-runtime/workspaceSession";
@@ -2,10 +2,11 @@ export type CommandEntry = {
2
2
  path: string[];
3
3
  script: string;
4
4
  description: string;
5
+ fixedArgs?: string[];
5
6
  };
6
7
 
7
8
  export const COMMANDS: CommandEntry[] = [
8
- { path: ["chat"], script: "chatWithAgent.ts", description: "Chat with an agent" },
9
+ { path: ["chat"], script: "", description: "Chat with an agent" },
9
10
 
10
11
  { path: ["doc", "create"], script: "createDoc.ts", description: "Create a normal doc" },
11
12
  { path: ["doc", "read"], script: "readDoc.ts", description: "Read a normal doc" },
@@ -19,24 +20,35 @@ export const COMMANDS: CommandEntry[] = [
19
20
  { path: ["skill", "create-doc"], script: "createSkillDoc.ts", description: "Create a skill-backed doc" },
20
21
 
21
22
  { path: ["dialog", "read"], script: "readDialog.ts", description: "Read a dialog" },
23
+ { path: ["dialog", "status"], script: "dialogStatus.ts", description: "Read compact dialog status" },
24
+ { path: ["space", "list"], script: "listSpaces.ts", description: "List joined spaces" },
22
25
  { path: ["space", "read"], script: "readSpace.ts", description: "Read a space" },
23
26
  { path: ["space", "delete"], script: "deleteSpaces.ts", description: "Delete spaces by safe filters" },
24
27
  { path: ["space", "category"], script: "upsertSpaceCategory.ts", description: "Create or update a space category" },
25
28
  { path: ["space", "content-category"], script: "setSpaceContentCategory.ts", description: "Move content into a space category" },
26
29
 
27
- { path: ["table", "data"], script: "tableData.ts", description: "Query or mutate table rows" },
30
+ { path: ["table", "query"], script: "tableData.ts", fixedArgs: ["--action", "query"], description: "Query table rows" },
31
+ { path: ["table", "add-column"], script: "tableData.ts", fixedArgs: ["--action", "add-column"], description: "Add one table column" },
32
+ { path: ["table", "add-row"], script: "tableData.ts", fixedArgs: ["--action", "add-row"], description: "Add one table row" },
33
+ { path: ["table", "add-rows"], script: "tableData.ts", fixedArgs: ["--action", "add-rows"], description: "Add table rows" },
34
+ { path: ["table", "update-row"], script: "tableData.ts", fixedArgs: ["--action", "update-row"], description: "Update one table row" },
35
+ { path: ["table", "update-rows"], script: "tableData.ts", fixedArgs: ["--action", "update-rows"], description: "Update table rows" },
36
+ { path: ["table", "delete-row"], script: "tableData.ts", fixedArgs: ["--action", "delete-row"], description: "Delete one table row" },
37
+ { path: ["table", "delete-rows"], script: "tableData.ts", fixedArgs: ["--action", "delete-rows"], description: "Delete table rows" },
38
+ { path: ["table", "data"], script: "tableData.ts", description: "Low-level table row script bridge" },
28
39
  { path: ["table", "meta"], script: "upsertTableMeta.ts", description: "Create or update table metadata" },
40
+ { path: ["task-run"], script: "", description: "Update durable task-run state through the runner bridge" },
29
41
 
30
42
  { path: ["agent", "list"], script: "listMyAgents.ts", description: "List owned agents" },
31
43
  { path: ["agent", "pull"], script: "", description: "Cache an agent for local runs" },
32
- { path: ["agent", "read"], script: "readAgent.ts", description: "Read a single agent" },
44
+ { path: ["agent", "read"], script: "", description: "Read a single agent" },
33
45
  { path: ["agent", "run"], script: "", description: "Run an agent" },
34
46
  { path: ["agent", "update"], script: "updateAgent.ts", description: "Update agent fields" },
35
47
  { path: ["agent", "bind-current"], script: "", description: "Bind an agent to this machine" },
36
48
  { path: ["agent", "smoke-current"], script: "", description: "Smoke test a bound agent through this machine" },
37
49
  { path: ["agent", "runtime-doctor"], script: "", description: "Diagnose current machine runtime compatibility" },
38
50
  { path: ["agent", "unpublish"], script: "unpublishAgent.ts", description: "Remove an agent's public record" },
39
- { path: ["agent", "chat"], script: "chatWithAgent.ts", description: "Chat with an agent" },
51
+ { path: ["agent", "chat"], script: "", description: "Chat with an agent" },
40
52
  { path: ["agent", "dialogs"], script: "queryAgentDialogs.ts", description: "Inspect recent dialogs for an agent" },
41
53
  { path: ["agent", "doctor"], script: "doctorAgentWorkspace.ts", description: "Audit agent workspace health" },
42
54
  { path: ["agent", "create-custom"], script: "createCustomCodingAgent.ts", description: "Create a custom coding agent" },
@@ -61,6 +73,7 @@ export const GROUP_ORDER = [
61
73
  "dialog",
62
74
  "space",
63
75
  "table",
76
+ "task-run",
64
77
  "llama",
65
78
  "model-runtime",
66
79
  "dev",
@@ -95,16 +108,20 @@ export function renderHelpText() {
95
108
  ' nolo doc create --title "Trip Notes" --body "hello" --sync local,us --dry-run',
96
109
  ' nolo skill-doc create --title "Agent Query Skill" --description "Inspect recent agent dialogs" --sync local,main,us',
97
110
  " nolo agent list --json",
111
+ " nolo space list --json",
98
112
  " nolo agent pull agent-pub-01APPBUILDER00000001YAII3I",
99
113
  " nolo agent read agent-pub-01APPBUILDER00000001YAII3I",
100
- ' nolo agent run frontend-implementer --msg "polish notifications" --local',
114
+ ' nolo agent run frontend-implementer --msg "polish notifications"',
101
115
  " nolo agent bind-current agent-user-1-agent-1",
102
116
  " nolo agent runtime-doctor agent-user-1-agent-1",
103
117
  ' nolo agent smoke-current agent-user-1-agent-1 --msg "ping"',
104
118
  ' nolo chat --agent agent-pub-01APPBUILDER00000001YAII3I --msg "你好"',
105
119
  " nolo space read 01KKY77TT0DA9NY7TNW3R7255N --content-key page-user-id --brief",
106
120
  " nolo space delete --name-prefix rn_owner_verify_0504 --yes",
107
- " nolo table data --table 01ABCXYZ --action query",
121
+ " nolo table query --table 01ABCXYZ",
122
+ ' nolo table query --table meta-b2e06f801f-NOLOTASKBOARD --columns \'["title","status","owner","priority","codeStatus"]\' --no-base-fields --output items',
123
+ ' nolo table update-row --table meta-b2e06f801f-NOLOTASKBOARD --row 01ROWID --changes \'{"status":"已完成"}\'',
124
+ " nolo task-run read-context --row-dbkey row-user-table-row",
108
125
  " nolo llama status",
109
126
  ];
110
127
 
@@ -0,0 +1,121 @@
1
+ import { existsSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import type { MachineRunPermissionPolicy } from "./ai/agent/machineRunPermissions";
4
+
5
+ type EnvLike = Record<string, string | undefined>;
6
+
7
+ export type ConnectorRunArtifact = {
8
+ cwd: string;
9
+ exitStatus: "completed" | "failed";
10
+ collectedAt: string;
11
+ baseSha?: string | null;
12
+ headSha?: string | null;
13
+ changedFiles?: string[];
14
+ statusShort?: string;
15
+ diffStat?: string;
16
+ patchPreview?: string;
17
+ error?: string;
18
+ artifactError?: string;
19
+ };
20
+
21
+ const MAX_PATCH_PREVIEW_CHARS = 24_000;
22
+
23
+ function normalizePath(value: string) {
24
+ return resolve(value.trim());
25
+ }
26
+
27
+ export function resolveConnectorRunCwd(args: {
28
+ env?: EnvLike;
29
+ policy: MachineRunPermissionPolicy;
30
+ fallbackCwd?: string;
31
+ }) {
32
+ const explicit =
33
+ args.env?.NOLO_CONNECTOR_WORKDIR ||
34
+ args.env?.NOLO_AGENT_WORKDIR ||
35
+ args.env?.NOLO_LOCAL_WORKTREE ||
36
+ "";
37
+ const candidate = explicit || args.policy.writableRoots[0] || args.fallbackCwd || process.cwd();
38
+ const cwd = normalizePath(candidate);
39
+ if (!existsSync(cwd)) {
40
+ throw new Error(`Connector run cwd does not exist: ${cwd}`);
41
+ }
42
+ return cwd;
43
+ }
44
+
45
+ async function runGit(
46
+ args: string[],
47
+ cwd: string,
48
+ options: { trim?: boolean } = {}
49
+ ): Promise<string | null> {
50
+ const proc = Bun.spawn(["git", ...args], {
51
+ cwd,
52
+ stdout: "pipe",
53
+ stderr: "pipe",
54
+ stdin: "ignore",
55
+ });
56
+ const [stdout, exitCode] = await Promise.all([
57
+ new Response(proc.stdout).text(),
58
+ proc.exited,
59
+ ]);
60
+ if (exitCode !== 0) return null;
61
+ return options.trim === false ? stdout.replace(/\r?\n$/, "") : stdout.trim();
62
+ }
63
+
64
+ export function parseStatusChangedFiles(statusShort: string | null): string[] {
65
+ if (!statusShort) return [];
66
+ return [...new Set(statusShort
67
+ .split(/\r?\n/)
68
+ .map((line) => {
69
+ const porcelainMatch = line.match(/^.{2} (.+)$/);
70
+ if (porcelainMatch?.[1]) return porcelainMatch[1].trim();
71
+ const trimmedMatch = line.match(/^[MADRCU?!]{1,2}\s+(.+)$/);
72
+ return trimmedMatch?.[1]?.trim() ?? "";
73
+ })
74
+ .map((line) => line.includes(" -> ") ? line.split(" -> ").at(-1) ?? line : line)
75
+ .filter(Boolean))];
76
+ }
77
+
78
+ export async function readConnectorGitHead(cwd: string): Promise<string | null> {
79
+ return runGit(["rev-parse", "HEAD"], cwd);
80
+ }
81
+
82
+ export async function collectConnectorRunArtifact(args: {
83
+ cwd: string;
84
+ baseSha?: string | null;
85
+ exitStatus: "completed" | "failed";
86
+ error?: string;
87
+ }): Promise<ConnectorRunArtifact> {
88
+ const collectedAt = new Date().toISOString();
89
+ try {
90
+ const [headSha, statusShort, diffStat, patchText] = await Promise.all([
91
+ readConnectorGitHead(args.cwd),
92
+ runGit(["status", "--short"], args.cwd, { trim: false }),
93
+ runGit(["diff", "--stat"], args.cwd),
94
+ runGit(["diff", "--no-color", "--unified=3"], args.cwd),
95
+ ]);
96
+ const patchPreview = patchText && patchText.length > MAX_PATCH_PREVIEW_CHARS
97
+ ? `${patchText.slice(0, MAX_PATCH_PREVIEW_CHARS)}\n...[truncated ${patchText.length - MAX_PATCH_PREVIEW_CHARS} chars]`
98
+ : patchText || undefined;
99
+ return {
100
+ cwd: args.cwd,
101
+ exitStatus: args.exitStatus,
102
+ collectedAt,
103
+ baseSha: args.baseSha ?? null,
104
+ headSha,
105
+ changedFiles: parseStatusChangedFiles(statusShort),
106
+ ...(statusShort ? { statusShort } : {}),
107
+ ...(diffStat ? { diffStat } : {}),
108
+ ...(patchPreview ? { patchPreview } : {}),
109
+ ...(args.error ? { error: args.error } : {}),
110
+ };
111
+ } catch (error) {
112
+ return {
113
+ cwd: args.cwd,
114
+ exitStatus: args.exitStatus,
115
+ collectedAt,
116
+ baseSha: args.baseSha ?? null,
117
+ ...(args.error ? { error: args.error } : {}),
118
+ artifactError: error instanceof Error ? error.message : String(error),
119
+ };
120
+ }
121
+ }
@@ -10,6 +10,16 @@ import {
10
10
  } from "./replication";
11
11
  import { toast } from "app/utils/toast";
12
12
 
13
+ const SPACE_MEMBER_PREFIX = "space-member-";
14
+
15
+ const getMemberUserIdFromSpaceMemberKey = (dbKey: string): string | null => {
16
+ if (!dbKey.startsWith(SPACE_MEMBER_PREFIX)) return null;
17
+ const rest = dbKey.slice(SPACE_MEMBER_PREFIX.length);
18
+ const lastDash = rest.lastIndexOf("-");
19
+ if (lastDash <= 0) return null;
20
+ return rest.slice(0, lastDash);
21
+ };
22
+
13
23
  // 辅助函数:保存到本地 DB
14
24
  const saveToClientDb = async (
15
25
  clientDb: any,
@@ -51,6 +61,10 @@ export const writeAction = async (
51
61
 
52
62
  const { data, customKey } = writeConfig;
53
63
  const userId = writeConfig.userId || currentUserId;
64
+ const isSpaceMemberRecord = customKey.startsWith(SPACE_MEMBER_PREFIX);
65
+ const recordUserId = isSpaceMemberRecord
66
+ ? data.userId || getMemberUserIdFromSpaceMemberKey(customKey) || userId
67
+ : userId;
54
68
 
55
69
  // 1. 基础参数校验
56
70
  if (!data || !customKey) {
@@ -87,7 +101,7 @@ export const writeAction = async (
87
101
  const willSaveData = normalizeTimeFields({
88
102
  ...data,
89
103
  dbKey: customKey,
90
- userId,
104
+ userId: recordUserId,
91
105
  });
92
106
 
93
107
  // 4. 本地保存
@@ -99,7 +113,7 @@ export const writeAction = async (
99
113
  const serverWriteConfig = {
100
114
  data: willSaveData,
101
115
  customKey,
102
- userId,
116
+ userId: recordUserId,
103
117
  };
104
118
 
105
119
  // 6. 后台异步同步到远程(若在线且有可用服务器)
@@ -42,6 +42,7 @@ interface UseUserDataReturn extends FetchState {
42
42
  interface UseUserDataOptions {
43
43
  allowPartialData?: boolean;
44
44
  partialDataStrategy?: PartialDataStrategy;
45
+ remoteSummary?: boolean;
45
46
  }
46
47
 
47
48
  export type PartialDataStrategy = "always" | "never" | "hydrated-cache";
@@ -164,11 +165,13 @@ const loadRemoteUserData = async ({
164
165
  queryUserId,
165
166
  types,
166
167
  limit,
168
+ summary,
167
169
  }: {
168
170
  serverOrigin: string;
169
171
  queryUserId: string;
170
172
  types: DataType[];
171
173
  limit: number;
174
+ summary?: boolean;
172
175
  }) => {
173
176
  try {
174
177
  const batchResponse = await noloQueryRequest({
@@ -176,7 +179,7 @@ const loadRemoteUserData = async ({
176
179
  queryUserId,
177
180
  options: {
178
181
  limit,
179
- condition: { type: types, includeDeleted: true },
182
+ condition: { type: types, includeDeleted: true, ...(summary ? { summary: true } : {}) },
180
183
  },
181
184
  });
182
185
 
@@ -198,7 +201,7 @@ const loadRemoteUserData = async ({
198
201
  queryUserId,
199
202
  options: {
200
203
  limit,
201
- condition: { type, includeDeleted: true },
204
+ condition: { type, includeDeleted: true, ...(summary ? { summary: true } : {}) },
202
205
  },
203
206
  });
204
207
  if (!response.ok) return [];
@@ -227,6 +230,7 @@ export function useUserData(
227
230
  if (options.partialDataStrategy) return options.partialDataStrategy;
228
231
  return options.allowPartialData ?? true ? "always" : "never";
229
232
  }, [options.allowPartialData, options.partialDataStrategy]);
233
+ const remoteSummary = options.remoteSummary === true;
230
234
 
231
235
  const [{ loading, error, data }, setState] = useState<FetchState>({
232
236
  loading: true,
@@ -383,6 +387,7 @@ export function useUserData(
383
387
  queryUserId: effectiveUserId,
384
388
  types: typeArray,
385
389
  limit,
390
+ summary: remoteSummary,
386
391
  })
387
392
  )
388
393
  );
@@ -415,7 +420,7 @@ export function useUserData(
415
420
  limitedAppKeys: summarizeAppKeys(limitedData),
416
421
  });
417
422
 
418
- if (mergedDataWithDeleted.length > 0) {
423
+ if (mergedDataWithDeleted.length > 0 && !remoteSummary) {
419
424
  try {
420
425
  await dispatch(cacheMergedUserDataThunk({ records: mergedDataWithDeleted })).unwrap();
421
426
  } catch (cacheError) {
@@ -462,6 +467,7 @@ export function useUserData(
462
467
  userId,
463
468
  typeArray,
464
469
  partialDataStrategy,
470
+ remoteSummary,
465
471
  allServers,
466
472
  ]);
467
473