@getpaseo/cli 0.1.96 → 0.1.97-beta.1

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/dist/cli.js CHANGED
@@ -9,6 +9,7 @@ import { createScheduleCommand } from "./commands/schedule/index.js";
9
9
  import { createSpeechCommand } from "./commands/speech/index.js";
10
10
  import { createTerminalCommand } from "./commands/terminal/index.js";
11
11
  import { createWorktreeCommand } from "./commands/worktree/index.js";
12
+ import { createHooksCommand } from "./commands/hooks.js";
12
13
  import { startCommand as daemonStartCommand } from "./commands/daemon/start.js";
13
14
  import { runStatusCommand as runDaemonStatusCommand } from "./commands/daemon/status.js";
14
15
  import { runRestartCommand as runDaemonRestartCommand } from "./commands/daemon/restart.js";
@@ -62,6 +63,7 @@ export function createCli() {
62
63
  // Top-level local daemon shortcuts
63
64
  program.addCommand(onboardCommand());
64
65
  program.addCommand(daemonStartCommand());
66
+ program.addCommand(createHooksCommand());
65
67
  addJsonOption(program
66
68
  .command("status")
67
69
  .description('Show local daemon status (alias for "paseo daemon status")'))
@@ -23,6 +23,7 @@ export interface AgentRunOptions extends CommandOptions {
23
23
  mode?: string;
24
24
  worktree?: string;
25
25
  base?: string;
26
+ workspace?: string;
26
27
  image?: string[];
27
28
  cwd?: string;
28
29
  env?: string[];
@@ -21,6 +21,7 @@ export function addRunOptions(cmd) {
21
21
  .option("--mode <mode>", "Provider-specific mode (e.g., plan, default, bypass)")
22
22
  .option("--worktree <name>", "Create agent in a new git worktree")
23
23
  .option("--base <branch>", "Base branch for worktree (default: current branch)")
24
+ .option("--workspace <id>", "Run in an existing workspace (default: a new workspace is created per run; falls back to $PASEO_WORKSPACE_ID)")
24
25
  .option("--image <path>", "Attach image(s) to the initial prompt (can be used multiple times)", collectMultiple, [])
25
26
  .option("--cwd <path>", "Working directory (default: current)")
26
27
  .option("--env <key=value>", "Set environment variable(s) for the agent process (can be used multiple times)", collectMultiple, [])
@@ -178,6 +179,20 @@ function validateRunOptions(prompt, options, outputSchema) {
178
179
  details: "Usage: paseo agent run --worktree <name> --base <branch> <prompt>",
179
180
  };
180
181
  }
182
+ if (options.worktree && options.workspace) {
183
+ throw {
184
+ code: "INVALID_OPTIONS",
185
+ message: "--worktree and --workspace cannot be combined",
186
+ details: "--workspace runs in an existing workspace; --worktree mints a new one",
187
+ };
188
+ }
189
+ if (options.worktree && !options.workspace && process.env.PASEO_WORKSPACE_ID) {
190
+ throw {
191
+ code: "INVALID_OPTIONS",
192
+ message: "--worktree cannot be combined with an ambient PASEO_WORKSPACE_ID",
193
+ details: "PASEO_WORKSPACE_ID selects an existing workspace; --worktree mints a new one. Unset PASEO_WORKSPACE_ID to use --worktree.",
194
+ };
195
+ }
181
196
  if (outputSchema && options.detach) {
182
197
  throw {
183
198
  code: "INVALID_OPTIONS",
@@ -274,6 +289,48 @@ async function connectToDaemonOrThrow(hostOption, host) {
274
289
  };
275
290
  }
276
291
  }
292
+ // Workspace policy for `paseo run`. Precedence:
293
+ // 1. --workspace <id> -> run in that existing workspace
294
+ // 2. $PASEO_WORKSPACE_ID -> exported by workspace terminals
295
+ // 3. --worktree <name> -> mint a new worktree-backed workspace
296
+ // 4. bare run -> mint a new local-backed workspace for cwd
297
+ // --worktree is rejected alongside both --workspace and an ambient
298
+ // $PASEO_WORKSPACE_ID (validateRunOptions), so worktree resolution here never
299
+ // races an existing-workspace selection.
300
+ async function resolveRunWorkspace(client, options, cwd) {
301
+ // An explicit --worktree mints its own workspace; --workspace and an ambient
302
+ // PASEO_WORKSPACE_ID are both rejected alongside --worktree upstream.
303
+ const explicit = options.worktree
304
+ ? undefined
305
+ : options.workspace?.trim() || process.env.PASEO_WORKSPACE_ID?.trim();
306
+ if (explicit) {
307
+ console.error(`Using workspace ${explicit}`);
308
+ return { id: explicit, cwd };
309
+ }
310
+ // TODO: thread the run `prompt` as firstAgentContext so workspace-level
311
+ // title/branch generation picks up the task description (U8/U6 deferred).
312
+ const result = options.worktree
313
+ ? await client.createWorkspace({
314
+ source: {
315
+ kind: "worktree",
316
+ cwd,
317
+ worktreeSlug: options.worktree,
318
+ baseBranch: options.base,
319
+ },
320
+ })
321
+ : await client.createWorkspace({ source: { kind: "directory", path: cwd } });
322
+ if (!result.workspace) {
323
+ throw {
324
+ code: "WORKSPACE_CREATE_FAILED",
325
+ message: result.error ?? "Failed to create workspace for this run",
326
+ };
327
+ }
328
+ const branch = result.workspace.gitRuntime?.currentBranch;
329
+ const label = branch ? `${result.workspace.name} (${branch})` : result.workspace.name;
330
+ console.error(`Created workspace ${result.workspace.id} - ${label}`);
331
+ console.error("Tip: pass --workspace <id> (or set PASEO_WORKSPACE_ID) to run in an existing workspace.");
332
+ return { id: result.workspace.id, cwd: result.workspace.workspaceDirectory ?? cwd };
333
+ }
277
334
  export async function runRunCommand(prompt, options, _command) {
278
335
  const host = getDaemonHost({ host: options.host });
279
336
  const outputSchema = options.outputSchema ? loadOutputSchema(options.outputSchema) : undefined;
@@ -295,23 +352,20 @@ export async function runRunCommand(prompt, options, _command) {
295
352
  throw error;
296
353
  }
297
354
  const images = loadRunImages(options.image);
298
- const git = options.worktree
299
- ? {
300
- createWorktree: true,
301
- worktreeSlug: options.worktree,
302
- baseBranch: options.base,
303
- }
304
- : undefined;
305
355
  const labels = parseRunLabels(options.label);
306
356
  const env = parseRunEnv(options.env);
307
357
  const requestEnv = Object.keys(env).length > 0 ? env : undefined;
358
+ const workspace = await resolveRunWorkspace(client, options, cwd);
359
+ const workspaceId = workspace.id;
360
+ const runCwd = workspace.cwd;
308
361
  if (outputSchema) {
309
362
  let structuredAgent = null;
310
363
  const callStructuredTurn = async (structuredPrompt) => {
311
364
  if (!structuredAgent) {
312
365
  structuredAgent = await client.createAgent({
313
366
  provider: resolvedProviderModel.provider,
314
- cwd,
367
+ cwd: runCwd,
368
+ workspaceId,
315
369
  title: resolvedTitle,
316
370
  modeId: options.mode,
317
371
  model: resolvedProviderModel.model,
@@ -320,8 +374,6 @@ export async function runRunCommand(prompt, options, _command) {
320
374
  outputSchema,
321
375
  images,
322
376
  env: requestEnv,
323
- git,
324
- worktreeName: options.worktree,
325
377
  labels: Object.keys(labels).length > 0 ? labels : undefined,
326
378
  });
327
379
  }
@@ -366,7 +418,8 @@ export async function runRunCommand(prompt, options, _command) {
366
418
  // Create the agent
367
419
  const agent = await client.createAgent({
368
420
  provider: resolvedProviderModel.provider,
369
- cwd,
421
+ cwd: runCwd,
422
+ workspaceId,
370
423
  title: resolvedTitle,
371
424
  modeId: options.mode,
372
425
  model: resolvedProviderModel.model,
@@ -374,8 +427,6 @@ export async function runRunCommand(prompt, options, _command) {
374
427
  initialPrompt: prompt,
375
428
  images,
376
429
  env: requestEnv,
377
- git,
378
- worktreeName: options.worktree,
379
430
  labels: Object.keys(labels).length > 0 ? labels : undefined,
380
431
  });
381
432
  // Default run behavior is foreground: wait for completion unless --detach is set.
@@ -0,0 +1,19 @@
1
+ import { Command } from "commander";
2
+ interface HookEnvironment {
3
+ PASEO_TERMINAL_ID?: string;
4
+ PASEO_ACTIVITY_TOKEN?: string;
5
+ PASEO_TERMINAL_ACTIVITY_URL?: string;
6
+ }
7
+ interface HookInput {
8
+ [Symbol.asyncIterator](): AsyncIterator<string | Buffer>;
9
+ isTTY?: boolean;
10
+ }
11
+ interface HooksRuntime {
12
+ env: HookEnvironment;
13
+ input: HookInput;
14
+ fetch: typeof fetch;
15
+ }
16
+ export declare function createHooksCommand(): Command;
17
+ export declare function runHooksCommand(agent: string, event: string, runtime?: HooksRuntime): Promise<void>;
18
+ export {};
19
+ //# sourceMappingURL=hooks.d.ts.map
@@ -0,0 +1,89 @@
1
+ import { Command } from "commander";
2
+ import { resolveHookActivity } from "@getpaseo/server/agent-hooks";
3
+ export function createHooksCommand() {
4
+ return new Command("hooks")
5
+ .description("Record agent hook activity")
6
+ .argument("<agent>", "Agent hook source")
7
+ .argument("<event>", "Agent hook event")
8
+ .action((agent, event) => runHooksCommand(agent, event));
9
+ }
10
+ export async function runHooksCommand(agent, event, runtime = {
11
+ env: process.env,
12
+ input: process.stdin,
13
+ fetch,
14
+ }) {
15
+ const target = resolveTarget(runtime.env);
16
+ if (!target)
17
+ return;
18
+ const state = await resolveHookActivity({
19
+ provider: agent,
20
+ event,
21
+ input: {
22
+ isTTY: runtime.input.isTTY,
23
+ read: () => readInput(runtime.input),
24
+ },
25
+ });
26
+ if (!state)
27
+ return;
28
+ await postActivity(target, state, runtime.fetch);
29
+ }
30
+ function resolveTarget(env) {
31
+ const terminalId = env.PASEO_TERMINAL_ID;
32
+ const token = env.PASEO_ACTIVITY_TOKEN;
33
+ const url = env.PASEO_TERMINAL_ACTIVITY_URL;
34
+ if (!terminalId || !token || !url)
35
+ return null;
36
+ return { terminalId, token, url };
37
+ }
38
+ async function readInput(input) {
39
+ const iterator = input[Symbol.asyncIterator]();
40
+ const chunks = [];
41
+ while (true) {
42
+ const next = await withTimeout(iterator.next(), 100);
43
+ if (!next) {
44
+ await iterator.return?.();
45
+ return null;
46
+ }
47
+ if (next.done)
48
+ return chunks.join("");
49
+ chunks.push(String(next.value));
50
+ }
51
+ }
52
+ async function withTimeout(promise, ms) {
53
+ let timeout;
54
+ try {
55
+ return await Promise.race([
56
+ promise,
57
+ new Promise((resolve) => {
58
+ timeout = setTimeout(() => resolve(null), ms);
59
+ }),
60
+ ]);
61
+ }
62
+ finally {
63
+ if (timeout)
64
+ clearTimeout(timeout);
65
+ }
66
+ }
67
+ async function postActivity(target, state, send) {
68
+ const controller = new AbortController();
69
+ const timeout = setTimeout(() => controller.abort(), 500);
70
+ try {
71
+ await send(target.url, {
72
+ method: "POST",
73
+ headers: { "content-type": "application/json" },
74
+ body: JSON.stringify({
75
+ terminalId: target.terminalId,
76
+ token: target.token,
77
+ state,
78
+ }),
79
+ signal: controller.signal,
80
+ });
81
+ }
82
+ catch {
83
+ return;
84
+ }
85
+ finally {
86
+ clearTimeout(timeout);
87
+ }
88
+ }
89
+ //# sourceMappingURL=hooks.js.map
@@ -4,7 +4,17 @@ export async function runCreateCommand(options, _command) {
4
4
  const { client } = await connectTerminalClient(options.host);
5
5
  const cwd = options.cwd ?? process.cwd();
6
6
  try {
7
- const payload = await client.createTerminal(cwd, options.name);
7
+ const opened = await client.openProject(cwd);
8
+ if (!opened.workspace) {
9
+ const error = {
10
+ code: "WORKSPACE_OPEN_FAILED",
11
+ message: opened.error ?? "Failed to open workspace",
12
+ };
13
+ throw error;
14
+ }
15
+ const payload = await client.createTerminal(cwd, options.name, undefined, {
16
+ workspaceId: opened.workspace.id,
17
+ });
8
18
  if (!payload.terminal) {
9
19
  const error = {
10
20
  code: "TERMINAL_CREATE_FAILED",
@@ -1,4 +1,5 @@
1
1
  import type { Command } from "commander";
2
+ import { connectToDaemon } from "../../utils/client.js";
2
3
  import type { CommandOptions, SingleResult, OutputSchema } from "../../output/index.js";
3
4
  /** Result type for worktree archive command */
4
5
  export interface WorktreeArchiveResult {
@@ -13,4 +14,7 @@ export interface WorktreeArchiveOptions extends CommandOptions {
13
14
  }
14
15
  export type WorktreeArchiveCommandResult = SingleResult<WorktreeArchiveResult>;
15
16
  export declare function runArchiveCommand(nameArg: string, options: WorktreeArchiveOptions, _command: Command): Promise<WorktreeArchiveCommandResult>;
17
+ export declare function runArchiveCommandWithDeps(nameArg: string, options: WorktreeArchiveOptions, deps: {
18
+ connectToDaemon: typeof connectToDaemon;
19
+ }): Promise<WorktreeArchiveCommandResult>;
16
20
  //# sourceMappingURL=archive.d.ts.map
@@ -13,6 +13,9 @@ export const archiveSchema = {
13
13
  ],
14
14
  };
15
15
  export async function runArchiveCommand(nameArg, options, _command) {
16
+ return runArchiveCommandWithDeps(nameArg, options, { connectToDaemon });
17
+ }
18
+ export async function runArchiveCommandWithDeps(nameArg, options, deps) {
16
19
  const host = getDaemonHost({ host: options.host });
17
20
  // Validate arguments
18
21
  if (!nameArg || nameArg.trim().length === 0) {
@@ -25,7 +28,7 @@ export async function runArchiveCommand(nameArg, options, _command) {
25
28
  }
26
29
  let client;
27
30
  try {
28
- client = await connectToDaemon({ host: options.host });
31
+ client = await deps.connectToDaemon({ host: options.host });
29
32
  }
30
33
  catch (err) {
31
34
  const message = err instanceof Error ? err.message : String(err);
@@ -59,9 +62,11 @@ export async function runArchiveCommand(nameArg, options, _command) {
59
62
  };
60
63
  throw error;
61
64
  }
62
- // Archive the worktree
65
+ // Archive the worktree. scope:"worktree" archives every active workspace on
66
+ // the directory and then removes the directory (Paseo-owned gated).
63
67
  const response = await client.archivePaseoWorktree({
64
68
  worktreePath: worktree.worktreePath,
69
+ scope: "worktree",
65
70
  });
66
71
  await client.close();
67
72
  if (response.error) {
@@ -30,7 +30,10 @@ export async function runCreateCommand(options, _command) {
30
30
  if (!workspace || response.error) {
31
31
  throw cmdError("WORKTREE_CREATE_FAILED", `Failed to create worktree: ${response.error ?? "no workspace returned"}`);
32
32
  }
33
- const worktreePath = workspace.workspaceDirectory ?? workspace.id;
33
+ if (!workspace.workspaceDirectory) {
34
+ throw cmdError("WORKTREE_CREATE_FAILED", "Failed to create worktree: workspace directory missing from daemon response");
35
+ }
36
+ const worktreePath = workspace.workspaceDirectory;
34
37
  return {
35
38
  type: "single",
36
39
  data: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getpaseo/cli",
3
- "version": "0.1.96",
3
+ "version": "0.1.97-beta.1",
4
4
  "description": "Paseo CLI - control your AI coding agents from the command line",
5
5
  "bin": {
6
6
  "paseo": "bin/paseo"
@@ -27,9 +27,9 @@
27
27
  },
28
28
  "dependencies": {
29
29
  "@clack/prompts": "^1.0.0",
30
- "@getpaseo/client": "0.1.96",
31
- "@getpaseo/protocol": "0.1.96",
32
- "@getpaseo/server": "0.1.96",
30
+ "@getpaseo/client": "0.1.97-beta.1",
31
+ "@getpaseo/protocol": "0.1.97-beta.1",
32
+ "@getpaseo/server": "0.1.97-beta.1",
33
33
  "chalk": "^5.3.0",
34
34
  "commander": "^12.0.0",
35
35
  "mime-types": "^2.1.35",