nolo-cli 0.1.17 → 0.1.18
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/agentAliases.ts +26 -0
- package/agentPullCommand.ts +2 -1
- package/agentRunCommand.ts +60 -3
- package/client/agentRun.ts +2 -0
- package/client/localRuntimeAdapter.test.ts +1 -2
- package/client/localRuntimeAdapter.ts +12 -3
- package/client/localToolPolicy.test.ts +1 -1
- package/client/localToolPolicy.ts +2 -6
- package/package.json +2 -1
package/agentAliases.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export const PLATFORM_DEMO_USER_ID = "b2e06f801f";
|
|
2
|
+
export const FRONTEND_IMPLEMENTER_AGENT_ID = "01FRONTENDAG0000000115N4E1";
|
|
3
|
+
export const FRONTEND_IMPLEMENTER_AGENT_KEY =
|
|
4
|
+
`agent-${PLATFORM_DEMO_USER_ID}-${FRONTEND_IMPLEMENTER_AGENT_ID}`;
|
|
5
|
+
|
|
6
|
+
const AGENT_ALIAS_TO_KEY: Record<string, string> = {
|
|
7
|
+
"frontend-implementer": FRONTEND_IMPLEMENTER_AGENT_KEY,
|
|
8
|
+
"frontend-agent": FRONTEND_IMPLEMENTER_AGENT_KEY,
|
|
9
|
+
frontend: FRONTEND_IMPLEMENTER_AGENT_KEY,
|
|
10
|
+
"front-end": FRONTEND_IMPLEMENTER_AGENT_KEY,
|
|
11
|
+
"前端": FRONTEND_IMPLEMENTER_AGENT_KEY,
|
|
12
|
+
"前端实现员": FRONTEND_IMPLEMENTER_AGENT_KEY,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
function parseAgentKeyFromInput(raw: string): string {
|
|
16
|
+
const trimmed = raw.trim();
|
|
17
|
+
if (trimmed.startsWith("http://") || trimmed.startsWith("https://")) {
|
|
18
|
+
const url = new URL(trimmed);
|
|
19
|
+
return url.pathname.replace(/^\/+/, "");
|
|
20
|
+
}
|
|
21
|
+
return trimmed;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function resolveCliAgentKeyInput(raw: string): string {
|
|
25
|
+
return AGENT_ALIAS_TO_KEY[raw.trim().toLowerCase()] ?? parseAgentKeyFromInput(raw);
|
|
26
|
+
}
|
package/agentPullCommand.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { DEFAULT_NOLO_SERVER_URL } from "./defaultServer";
|
|
2
2
|
import { getDefaultCliLocalRuntimeDb } from "./localRuntimeDb";
|
|
3
3
|
import type { CliLocalRuntimeDb } from "./client/localRuntimeAdapter";
|
|
4
|
+
import { resolveCliAgentKeyInput } from "./agentAliases";
|
|
4
5
|
|
|
5
6
|
type EnvLike = Record<string, string | undefined>;
|
|
6
7
|
|
|
@@ -40,7 +41,7 @@ function positionalArgs(args: string[]) {
|
|
|
40
41
|
export function parseAgentPullArgs(args: string[]): ParsedAgentPullArgs | null {
|
|
41
42
|
const agentKey = readFlagValue(args, "--agent") ?? positionalArgs(args)[0];
|
|
42
43
|
if (!agentKey?.trim()) return null;
|
|
43
|
-
return { agentKey: agentKey
|
|
44
|
+
return { agentKey: resolveCliAgentKeyInput(agentKey) };
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
function resolveServerUrl(env: EnvLike) {
|
package/agentRunCommand.ts
CHANGED
|
@@ -2,7 +2,8 @@ import { DEFAULT_NOLO_SERVER_URL } from "./defaultServer";
|
|
|
2
2
|
import { runAgentTurn, type RunAgentTurnResult } from "./client/agentRun";
|
|
3
3
|
import type { AgentRuntimeRequestedMode } from "./agentRuntimeLocal";
|
|
4
4
|
import { existsSync, readFileSync } from "node:fs";
|
|
5
|
-
import { extname, resolve } from "node:path";
|
|
5
|
+
import { extname, join, resolve } from "node:path";
|
|
6
|
+
import { resolveCliAgentKeyInput } from "./agentAliases";
|
|
6
7
|
|
|
7
8
|
type EnvLike = Record<string, string | undefined>;
|
|
8
9
|
|
|
@@ -15,6 +16,7 @@ type AgentRunCommandDeps = {
|
|
|
15
16
|
scriptDir: string;
|
|
16
17
|
output?: OutputLike;
|
|
17
18
|
runner?: typeof runAgentTurn;
|
|
19
|
+
prepareShellWorktree?: typeof prepareShellWorktree;
|
|
18
20
|
};
|
|
19
21
|
|
|
20
22
|
type ParsedAgentRunArgs = {
|
|
@@ -82,7 +84,7 @@ export function parseAgentRunArgs(args: string[]): ParsedAgentRunArgs | null {
|
|
|
82
84
|
...readRepeatedFlagValues(args, "--image-url"),
|
|
83
85
|
];
|
|
84
86
|
return {
|
|
85
|
-
agentKey,
|
|
87
|
+
agentKey: resolveCliAgentKeyInput(agentKey),
|
|
86
88
|
message: message.trim(),
|
|
87
89
|
imageUrls,
|
|
88
90
|
allowShell: args.includes("--dangerously-allow-shell"),
|
|
@@ -117,6 +119,49 @@ function resolveServerUrl(env: EnvLike) {
|
|
|
117
119
|
return (env.NOLO_SERVER || env.BASE_URL || DEFAULT_NOLO_SERVER_URL).replace(/\/+$/, "");
|
|
118
120
|
}
|
|
119
121
|
|
|
122
|
+
function safePathSegment(input: string) {
|
|
123
|
+
return input.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80) || "agent";
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function createTaskId() {
|
|
127
|
+
return Date.now().toString(36).toUpperCase();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function runGit(args: string[], cwd: string) {
|
|
131
|
+
const proc = Bun.spawn(["git", ...args], {
|
|
132
|
+
cwd,
|
|
133
|
+
stdout: "pipe",
|
|
134
|
+
stderr: "pipe",
|
|
135
|
+
stdin: "ignore",
|
|
136
|
+
});
|
|
137
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
138
|
+
new Response(proc.stdout).text(),
|
|
139
|
+
new Response(proc.stderr).text(),
|
|
140
|
+
proc.exited,
|
|
141
|
+
]);
|
|
142
|
+
if (exitCode !== 0) {
|
|
143
|
+
throw new Error(stderr.trim() || stdout.trim() || `git ${args.join(" ")} failed`);
|
|
144
|
+
}
|
|
145
|
+
return stdout.trim();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export async function prepareShellWorktree(args: {
|
|
149
|
+
agentKey: string;
|
|
150
|
+
cwd?: string;
|
|
151
|
+
env?: EnvLike;
|
|
152
|
+
}) {
|
|
153
|
+
if (args.env?.NOLO_LOCAL_WORKTREE) return args.env.NOLO_LOCAL_WORKTREE;
|
|
154
|
+
const cwd = args.cwd ?? process.cwd();
|
|
155
|
+
const root = await runGit(["rev-parse", "--show-toplevel"], cwd);
|
|
156
|
+
const parent = join(root, ".worktrees");
|
|
157
|
+
const safeAgent = safePathSegment(args.agentKey);
|
|
158
|
+
const taskId = createTaskId();
|
|
159
|
+
const worktreePath = join(parent, `nolo-agent-${safeAgent}-${taskId}`);
|
|
160
|
+
const branchName = `nolo-agent-${safeAgent}-${taskId}`;
|
|
161
|
+
await runGit(["worktree", "add", worktreePath, "-b", branchName, "HEAD"], root);
|
|
162
|
+
return worktreePath;
|
|
163
|
+
}
|
|
164
|
+
|
|
120
165
|
function writeUsage(output: OutputLike) {
|
|
121
166
|
output.write(
|
|
122
167
|
"Usage: nolo agent run <agent> <message> [--local|--server|--auto] [--continue <dialogId>]\n" +
|
|
@@ -134,8 +179,19 @@ export async function runAgentRunCommand(args: string[], deps: AgentRunCommandDe
|
|
|
134
179
|
}
|
|
135
180
|
|
|
136
181
|
const runner = deps.runner ?? runAgentTurn;
|
|
182
|
+
let localRuntimeCwd: string | undefined;
|
|
183
|
+
if (parsed.allowShell) {
|
|
184
|
+
localRuntimeCwd = await (deps.prepareShellWorktree ?? prepareShellWorktree)({
|
|
185
|
+
agentKey: parsed.agentKey,
|
|
186
|
+
env,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
137
189
|
const runEnv = parsed.allowShell
|
|
138
|
-
? {
|
|
190
|
+
? {
|
|
191
|
+
...env,
|
|
192
|
+
NOLO_LOCAL_SHELL_MODE: "worktree",
|
|
193
|
+
...(localRuntimeCwd ? { NOLO_LOCAL_WORKTREE: localRuntimeCwd } : {}),
|
|
194
|
+
}
|
|
139
195
|
: env;
|
|
140
196
|
const result: RunAgentTurnResult = await runner({
|
|
141
197
|
agentName: parsed.agentKey,
|
|
@@ -146,6 +202,7 @@ export async function runAgentRunCommand(args: string[], deps: AgentRunCommandDe
|
|
|
146
202
|
scriptDir: deps.scriptDir,
|
|
147
203
|
env: runEnv,
|
|
148
204
|
output,
|
|
205
|
+
...(localRuntimeCwd ? { localRuntimeCwd } : {}),
|
|
149
206
|
...(parsed.runtimeMode ? { runtimeMode: parsed.runtimeMode } : {}),
|
|
150
207
|
...(parsed.continueDialogId ? { continueDialogId: parsed.continueDialogId } : {}),
|
|
151
208
|
});
|
package/client/agentRun.ts
CHANGED
|
@@ -24,6 +24,7 @@ type RunAgentTurnOptions = {
|
|
|
24
24
|
runtimeMode?: AgentRuntimeRequestedMode;
|
|
25
25
|
localRuntimeAdapter?: AgentRuntimeHostAdapter;
|
|
26
26
|
localRuntimeAdapterFactory?: (env: EnvLike) => AgentRuntimeHostAdapter;
|
|
27
|
+
localRuntimeCwd?: string;
|
|
27
28
|
scriptPathExists?: (path: string) => boolean;
|
|
28
29
|
fetchImpl?: typeof fetch;
|
|
29
30
|
};
|
|
@@ -61,6 +62,7 @@ function buildDefaultLocalRuntimeAdapter(options: RunAgentTurnOptions) {
|
|
|
61
62
|
return createCliLocalRuntimeAdapter({
|
|
62
63
|
env: options.env,
|
|
63
64
|
fetchImpl: options.fetchImpl,
|
|
65
|
+
cwd: options.localRuntimeCwd,
|
|
64
66
|
});
|
|
65
67
|
}
|
|
66
68
|
|
|
@@ -215,7 +215,7 @@ describe("CLI local runtime adapter", () => {
|
|
|
215
215
|
expect(result.content).toContain("README.md");
|
|
216
216
|
});
|
|
217
217
|
|
|
218
|
-
test("advertises execShell to OpenAI-compatible providers when
|
|
218
|
+
test("advertises execShell to OpenAI-compatible providers when shell mode is enabled", async () => {
|
|
219
219
|
const requests: Array<{ body: any }> = [];
|
|
220
220
|
const adapter = createCliLocalRuntimeAdapter({
|
|
221
221
|
env: {
|
|
@@ -228,7 +228,6 @@ describe("CLI local runtime adapter", () => {
|
|
|
228
228
|
dbKey: "agent-local-shell",
|
|
229
229
|
prompt: "Use shell.",
|
|
230
230
|
model: "gpt-4.1-mini",
|
|
231
|
-
toolNames: ["execShell"],
|
|
232
231
|
}),
|
|
233
232
|
put: async () => {},
|
|
234
233
|
batch: async () => {},
|
|
@@ -67,8 +67,14 @@ function buildExecShellTool() {
|
|
|
67
67
|
};
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
function
|
|
71
|
-
return
|
|
70
|
+
function shellModeEnabled(env: EnvLike) {
|
|
71
|
+
return ["worktree", "dangerous", "1", "true"].includes(env.NOLO_LOCAL_SHELL_MODE || "");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function buildOpenAiTools(args: { toolNames?: string[]; env: EnvLike }) {
|
|
75
|
+
return args.toolNames?.includes("execShell") || shellModeEnabled(args.env)
|
|
76
|
+
? [buildExecShellTool()]
|
|
77
|
+
: [];
|
|
72
78
|
}
|
|
73
79
|
|
|
74
80
|
function parseToolArguments(raw: string) {
|
|
@@ -291,7 +297,10 @@ export function createCliLocalRuntimeAdapter(
|
|
|
291
297
|
resolveProvider: async (agentConfig) => ({
|
|
292
298
|
model: agentConfig.model || "gpt-4.1-mini",
|
|
293
299
|
complete: async (messages) => {
|
|
294
|
-
const tools = buildOpenAiTools(
|
|
300
|
+
const tools = buildOpenAiTools({
|
|
301
|
+
toolNames: agentConfig.toolNames,
|
|
302
|
+
env: deps.env,
|
|
303
|
+
});
|
|
295
304
|
const res = await fetchImpl(`${resolveOpenAiCompatibleBaseUrl(deps.env)}/chat/completions`, {
|
|
296
305
|
method: "POST",
|
|
297
306
|
headers: {
|
|
@@ -6,7 +6,7 @@ describe("CLI local tool policy", () => {
|
|
|
6
6
|
test("allows execShell in explicit worktree shell mode", () => {
|
|
7
7
|
expect(resolveLocalToolPolicy({
|
|
8
8
|
env: { NOLO_LOCAL_SHELL_MODE: "worktree" },
|
|
9
|
-
agentToolNames: [
|
|
9
|
+
agentToolNames: [],
|
|
10
10
|
toolName: "execShell",
|
|
11
11
|
})).toEqual({ allowed: true, toolName: "execShell" });
|
|
12
12
|
});
|
|
@@ -27,17 +27,13 @@ export function resolveLocalToolPolicy(args: {
|
|
|
27
27
|
}): LocalToolPolicyDecision {
|
|
28
28
|
if (args.toolName === "execShell") {
|
|
29
29
|
const shellMode = args.env.NOLO_LOCAL_SHELL_MODE || "";
|
|
30
|
-
|
|
31
|
-
if (
|
|
32
|
-
agentTools.has("execShell") &&
|
|
33
|
-
["worktree", "dangerous", "1", "true"].includes(shellMode)
|
|
34
|
-
) {
|
|
30
|
+
if (["worktree", "dangerous", "1", "true"].includes(shellMode)) {
|
|
35
31
|
return { allowed: true, toolName: args.toolName };
|
|
36
32
|
}
|
|
37
33
|
return {
|
|
38
34
|
allowed: false,
|
|
39
35
|
toolName: args.toolName,
|
|
40
|
-
reason: "execShell requires NOLO_LOCAL_SHELL_MODE=worktree
|
|
36
|
+
reason: "execShell requires NOLO_LOCAL_SHELL_MODE=worktree.",
|
|
41
37
|
};
|
|
42
38
|
}
|
|
43
39
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nolo-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.18",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Agent-first terminal workspace for Nolo",
|
|
6
6
|
"bin": {
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"files": [
|
|
11
11
|
"index.ts",
|
|
12
12
|
"agentRuntimeLocal.ts",
|
|
13
|
+
"agentAliases.ts",
|
|
13
14
|
"localRuntimeDb.ts",
|
|
14
15
|
"agentRuntimeCommands.ts",
|
|
15
16
|
"agentPullCommand.ts",
|