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.
@@ -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
+ }
@@ -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.trim() };
44
+ return { agentKey: resolveCliAgentKeyInput(agentKey) };
44
45
  }
45
46
 
46
47
  function resolveServerUrl(env: EnvLike) {
@@ -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
- ? { ...env, NOLO_LOCAL_SHELL_MODE: "worktree" }
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
  });
@@ -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 the agent declares it", async () => {
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 buildOpenAiTools(toolNames: string[] = []) {
71
- return toolNames.includes("execShell") ? [buildExecShellTool()] : [];
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(agentConfig.toolNames ?? []);
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: ["execShell"],
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
- const agentTools = new Set(args.agentToolNames ?? []);
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 and an agent that declares execShell.",
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.17",
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",