nolo-cli 0.1.19 → 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.
- package/README.md +9 -1
- package/agent-runtime/agentConfigOptions.ts +12 -0
- package/agent-runtime/agentRecordConfig.ts +99 -0
- package/agent-runtime/agentRecordKeys.ts +14 -0
- package/agent-runtime/dialogMessageRecord.ts +16 -0
- package/agent-runtime/dialogWritePlan.ts +130 -0
- package/agent-runtime/hostAdapter.ts +13 -0
- package/agent-runtime/hybridRecordStore.ts +147 -0
- package/agent-runtime/index.ts +69 -0
- package/agent-runtime/localLoop.ts +69 -5
- package/agent-runtime/localToolPolicy.ts +130 -0
- package/agent-runtime/localWorkspaceTools.ts +1532 -0
- package/agent-runtime/openAiCompatibleProvider.ts +70 -0
- package/agent-runtime/openAiCompatibleProviderConfig.ts +38 -0
- package/agent-runtime/platformChatProvider.ts +241 -0
- package/agent-runtime/taskWorkspace.ts +193 -0
- package/agent-runtime/types.ts +1 -0
- package/agent-runtime/workspaceSession.ts +76 -0
- package/agentAliases.ts +37 -0
- package/agentPullCommand.ts +1 -1
- package/agentRunCommand.ts +278 -52
- package/agentRuntimeCommands.ts +354 -164
- package/agentRuntimeLocal.ts +38 -0
- package/ai/agent/agentSlice.ts +10 -0
- package/ai/agent/buildEditingContext.ts +5 -0
- package/ai/agent/buildSystemPrompt.ts +41 -18
- package/ai/agent/canvasEditingContext.ts +49 -0
- package/ai/agent/cliExecutor.ts +15 -4
- package/ai/agent/createAgentSchema.ts +2 -0
- package/ai/agent/executeToolCall.ts +3 -2
- package/ai/agent/hooks/usePublicAgents.ts +6 -0
- package/ai/agent/pageBuilderHandoffRules.ts +75 -0
- package/ai/agent/runAgentClientLoop.ts +4 -1
- package/ai/agent/runtimeGuidance.ts +19 -0
- package/ai/agent/server/fetchPublicAgents.ts +51 -1
- package/ai/agent/streamAgentChatTurn.ts +20 -2
- package/ai/agent/streamAgentChatTurnUtils.ts +60 -16
- package/ai/chat/accumulateToolCallChunks.ts +40 -9
- package/ai/chat/parseApiError.ts +3 -0
- package/ai/chat/sendOpenAICompletionsRequest.native.ts +23 -10
- package/ai/chat/sendOpenAICompletionsRequest.ts +13 -1
- package/ai/chat/updateTotalUsage.ts +26 -9
- package/ai/llm/deepinfra.ts +51 -0
- package/ai/llm/getPricing.ts +6 -0
- package/ai/llm/kimi.ts +2 -0
- package/ai/llm/openrouterModels.ts +0 -135
- package/ai/llm/providers.ts +1 -0
- package/ai/llm/types.ts +8 -0
- package/ai/taskRun/taskRunProtocol.ts +823 -0
- package/ai/token/calculatePrice.ts +30 -0
- package/ai/token/externalToolCost.ts +49 -29
- package/ai/token/prepareTokenUsageData.ts +6 -1
- package/ai/token/serverTokenWriter.ts +4 -2
- package/ai/tools/agent/agentTools.ts +21 -0
- package/ai/tools/agent/presets/appBuilderPreset.ts +7 -0
- package/ai/tools/agent/streamParallelAgentsTool.ts +2 -1
- package/ai/tools/agent/taskRunTool.ts +112 -0
- package/ai/tools/applyEditTool.ts +6 -3
- package/ai/tools/applyLineEditsTool.ts +6 -3
- package/ai/tools/checkEnvTool.ts +14 -9
- package/ai/tools/codeSearchTool.ts +17 -5
- package/ai/tools/execBashTool.ts +33 -29
- package/ai/tools/fetchWebpageSupport.ts +24 -0
- package/ai/tools/fetchWebpageTool.ts +18 -5
- package/ai/tools/index.ts +158 -0
- package/ai/tools/jdProductScraperTool.ts +821 -0
- package/ai/tools/listFilesTool.ts +6 -3
- package/ai/tools/localFilesTool.ts +200 -0
- package/ai/tools/readFileTool.ts +6 -3
- package/ai/tools/searchRepoTool.ts +6 -3
- package/ai/tools/table/rowTools.ts +6 -1
- package/ai/tools/taobaoTmallProductScraperTool.ts +49 -0
- package/ai/tools/toolApiClient.ts +20 -6
- package/ai/tools/wereadGatewayTool.ts +152 -0
- package/ai/tools/writeFileTool.ts +6 -3
- package/client/agentConfigResolver.test.ts +70 -0
- package/client/agentConfigResolver.ts +1 -0
- package/client/agentRun.test.ts +361 -7
- package/client/agentRun.ts +449 -63
- package/client/hybridRecordStore.test.ts +115 -0
- package/client/hybridRecordStore.ts +41 -0
- package/client/localAgentRecords.test.ts +27 -0
- package/client/localAgentRecords.ts +7 -0
- package/client/localDialogRecords.test.ts +124 -0
- package/client/localDialogRecords.ts +30 -0
- package/client/localProviderResolver.test.ts +78 -0
- package/client/localProviderResolver.ts +1 -0
- package/client/localRuntimeAdapter.test.ts +621 -9
- package/client/localRuntimeAdapter.ts +275 -250
- package/client/localRuntimeDryRun.test.ts +116 -0
- package/client/localToolPolicy.ts +8 -81
- package/client/taskRunPrompt.ts +26 -0
- package/client/taskWorktree.ts +8 -0
- package/client/workspaceSession.test.ts +57 -0
- package/client/workspaceSession.ts +11 -0
- package/commandRegistry.ts +23 -6
- package/connectorRunArtifact.ts +121 -0
- package/database/actions/write.ts +16 -2
- package/database/hooks/useUserData.ts +9 -3
- package/database/server/dataHandlers.ts +18 -20
- package/database/server/emailRepository.ts +3 -3
- package/database/server/patch.ts +18 -10
- package/database/server/query.ts +43 -4
- package/database/server/read.ts +24 -38
- package/database/server/recordIdentity.ts +100 -0
- package/database/server/write.ts +21 -25
- package/index.ts +70 -33
- package/machineCommands.ts +318 -144
- package/package.json +4 -1
- package/tableCommands.ts +181 -0
- 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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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";
|
package/commandRegistry.ts
CHANGED
|
@@ -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: "
|
|
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", "
|
|
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: "
|
|
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: "
|
|
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"
|
|
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
|
|
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
|
|