skydive-cli 0.1.0-beta.341 → 0.1.0-beta.347

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 CHANGED
@@ -70,6 +70,26 @@ skydive portal status
70
70
 
71
71
  Global flags: `--json`, `--quiet`, `--api-url <url>`.
72
72
 
73
+ ## Workspaces
74
+
75
+ Every `skydive` command acts inside one **workspace** — the org/team boundary
76
+ that owns your agents, conversations, and secrets. If your account belongs to
77
+ several workspaces (say, your company org and a personal one), the same agent
78
+ name can exist in both, so which workspace is active determines what every
79
+ command sees.
80
+
81
+ - `skydive workspace list` — workspaces on your account, with the active one
82
+ marked.
83
+ - `skydive workspace switch [workspace]` — change the active workspace. All
84
+ commands — `agents`, `secrets`, `keys`, `chat`, `conversations` — follow it.
85
+ - `skydive auth status` — shows who you're logged in as and which workspace is
86
+ active.
87
+ - In the chat TUI, `ctrl+w` switches workspace from the agent picker.
88
+
89
+ The active workspace is stored in the CLI config file and persists across
90
+ invocations. If a command can't find an agent you know exists, check
91
+ `skydive auth status` first — you're probably in the wrong workspace.
92
+
73
93
  ## `skydive secrets`
74
94
 
75
95
  Manage the environment secrets injected into an agent's sandbox. All
@@ -250,7 +270,7 @@ environments, and driving agents from other tools:
250
270
  ```sh
251
271
  skydive portal open # connect and stay in the foreground (ctrl+c to close)
252
272
  skydive portal open --agent grace # …and grant that agent on first connect
253
- skydive portal open --cwd ~/some/repo # working directory for the agent's commands
273
+ skydive portal open --cwd ~/some/repo # working directory for the agent's commands (a starting point, not a boundary — see above)
254
274
  skydive portal grant --agent grace # allow an agent (registers this machine if needed)
255
275
  skydive portal revoke --agent grace # remove an agent's access
256
276
  skydive portal status # machines, connection state, granted agents
package/dist/js/bin.mjs CHANGED
@@ -16,7 +16,7 @@ import zlib from "node:zlib";
16
16
  import os from "node:os";
17
17
 
18
18
  //#region package.json
19
- var version$1 = "0.1.0-beta.341";
19
+ var version$1 = "0.1.0-beta.347";
20
20
 
21
21
  //#endregion
22
22
  //#region src/types.ts
@@ -1171,11 +1171,14 @@ const conversationSummarySchema = z.object({
1171
1171
  title: z.string().nullable().optional()
1172
1172
  })
1173
1173
  });
1174
- const conversationTitleSchema = z.object({
1174
+ const conversationDetailSchema = z.object({
1175
1175
  id: z.string().uuid(),
1176
- title: z.string().nullable()
1176
+ title: z.string().nullable(),
1177
+ agentId: z.string().uuid(),
1178
+ createdAt: z.string(),
1179
+ updatedAt: z.string()
1177
1180
  });
1178
- const getConversationResponseSchema = z.object({ conversation: conversationTitleSchema });
1181
+ const getConversationResponseSchema = z.object({ conversation: conversationDetailSchema });
1179
1182
  const listConversationsResponseSchema = z.object({
1180
1183
  conversations: z.array(conversationSummarySchema),
1181
1184
  nextCursor: z.string().nullable().optional(),
@@ -2570,8 +2573,9 @@ const chatCommand = {
2570
2573
  type: "string",
2571
2574
  describe: "Target agent, by id, slug, or name. With -p, the agent to send the one-shot prompt to. Without -p, pre-selects the agent and opens its conversation list, skipping the agent picker. Optional when the account has exactly one agent."
2572
2575
  }).option("conversation", {
2576
+ alias: "resume",
2573
2577
  type: "string",
2574
- describe: "For -p: continue an existing conversation by id instead of starting a new one."
2578
+ describe: "Continue an existing conversation by id. With -p, the one-shot lands in that conversation; without -p, the TUI opens directly into it (the agent is resolved from the conversation, no --agent needed)."
2575
2579
  }).option("share-machine", {
2576
2580
  type: "boolean",
2577
2581
  describe: "Share this machine with the agent over the portal so it can run commands here (default-deny; in the TUI you approve per agent, with -p the flag grants the target agent for the run). Defaults to `shareMachineDefault` in the CLI config file; --no-share-machine disables for this invocation."
@@ -2582,7 +2586,7 @@ const chatCommand = {
2582
2586
  type: "boolean",
2583
2587
  default: true,
2584
2588
  describe: "Show a desktop notification when a run finishes or needs your input while this terminal is unfocused (use --no-notify to disable)"
2585
- }).example("skydive chat -p \"summarize my open PRs\" --agent grace", "One-shot, non-interactive").example("echo \"what changed today?\" | skydive chat -p --agent grace", "Read the prompt from stdin").example("skydive chat --agent grace", "Open Grace's conversation list in the TUI (skips the agent picker)"),
2589
+ }).example("skydive chat -p \"summarize my open PRs\" --agent grace", "One-shot, non-interactive").example("echo \"what changed today?\" | skydive chat -p --agent grace", "Read the prompt from stdin").example("skydive chat --agent grace", "Open Grace's conversation list in the TUI (skips the agent picker)").example("skydive chat --resume 0197e0f3-…", "Reopen a previous conversation in the TUI (the id is printed when you quit a chat)"),
2586
2590
  handler: async (argv) => {
2587
2591
  const appUrl = resolveAppUrl({ appUrl: argv["api-url"] });
2588
2592
  if (argv.print !== void 0) {
@@ -2618,7 +2622,7 @@ const chatCommand = {
2618
2622
  printError(`Unknown theme "${themeId}". Valid themes: ${themes.map((t) => t.id).join(", ")}`);
2619
2623
  process.exit(1);
2620
2624
  }
2621
- const { runChat } = await import("./boot-Dr7pEKqB.mjs");
2625
+ const { runChat } = await import("./boot-BcuOicbb.mjs");
2622
2626
  await runChat({
2623
2627
  appUrl,
2624
2628
  sessionToken: session.value.sessionToken,
@@ -2626,7 +2630,8 @@ const chatCommand = {
2626
2630
  promptHistoryPath: getPromptHistoryPath(),
2627
2631
  theme: themeId,
2628
2632
  notifications: argv.notify,
2629
- agentSelector: argv.agent ?? null
2633
+ agentSelector: argv.agent ?? null,
2634
+ conversationId: argv.conversation ?? null
2630
2635
  });
2631
2636
  }
2632
2637
  };
@@ -3411,7 +3416,7 @@ const switchCommand = {
3411
3416
  printError("The workspace picker needs the Bun runtime and it could not be set up automatically. Pass a workspace slug instead, or install Bun and retry.");
3412
3417
  process.exit(1);
3413
3418
  }
3414
- const { runWorkspacePicker } = await import("./boot-Dr7pEKqB.mjs");
3419
+ const { runWorkspacePicker } = await import("./boot-BcuOicbb.mjs");
3415
3420
  await runWorkspacePicker(session);
3416
3421
  return;
3417
3422
  }
@@ -3795,6 +3800,7 @@ const VALUE_FLAGS = new Set([
3795
3800
  "-p",
3796
3801
  "--agent",
3797
3802
  "--conversation",
3803
+ "--resume",
3798
3804
  "--theme",
3799
3805
  "--api-url"
3800
3806
  ]);
@@ -137,18 +137,56 @@ const useStore = create((set, get) => ({
137
137
  /**
138
138
  * Decide the screen the chat TUI boots into.
139
139
  *
140
- * With no `--agent` selector we open the agent picker, exactly as before.
141
- * With a selector (`skydive chat --agent grace`, no `-p`) we resolve it
140
+ * With no selector we open the agent picker, exactly as before.
141
+ *
142
+ * With `--agent` (`skydive chat --agent grace`, no `-p`) we resolve it
142
143
  * against the org roster and open that agent's conversation list, skipping
143
144
  * the picker. Resolution reuses the same `resolveAgent` rules as the `-p`
144
145
  * path so a given selector behaves identically in both modes: an id, or a
145
146
  * unique case-insensitive slug/name; ambiguous or unknown selectors throw
146
147
  * with the candidate list.
147
148
  *
149
+ * With `--resume <conversation-id>` we skip both pickers and boot straight
150
+ * into that conversation. The id alone is enough — the owning agent is
151
+ * recovered from the conversation itself — so the "Resume this conversation
152
+ * with: …" hint printed on exit round-trips without the user also passing
153
+ * `--agent`. When both are given, the conversation wins (it pins the agent
154
+ * anyway).
155
+ *
148
156
  * Kept out of the React component so it's unit-testable under Node (the
149
157
  * component itself can't render without Bun + OpenTUI).
150
158
  */
151
- async function resolveInitialScreen({ rest, agentSelector }) {
159
+ async function resolveInitialScreen({ rest, agentSelector, conversationId }) {
160
+ if (conversationId) {
161
+ const [conversation, agents] = await Promise.all([rest.getConversation({ conversationId }).catch((err) => {
162
+ if (err instanceof HttpError && err.status === 404) throw new Error(`No conversation with id ${conversationId} — it may have been deleted, or belong to another workspace.`);
163
+ throw err;
164
+ }), rest.listAgents({
165
+ scope: "org",
166
+ onPage: null
167
+ })]);
168
+ const agent = agents.find((a) => a.id === conversation.agentId);
169
+ if (!agent) throw new Error(`Conversation ${conversationId} belongs to an agent you can't access.`);
170
+ return {
171
+ kind: "chat",
172
+ agent,
173
+ conversation: {
174
+ id: conversation.id,
175
+ title: conversation.title,
176
+ createdAt: conversation.createdAt,
177
+ updatedAt: conversation.updatedAt,
178
+ preview: null,
179
+ channel: null,
180
+ channelLabel: null,
181
+ agent: {
182
+ id: agent.id,
183
+ name: agent.name,
184
+ slug: agent.slug ?? null,
185
+ title: agent.title ?? null
186
+ }
187
+ }
188
+ };
189
+ }
152
190
  if (!agentSelector) return { kind: "agent-picker" };
153
191
  return {
154
192
  kind: "conversation-picker",
@@ -4337,6 +4375,39 @@ function filterSlashCommands(query) {
4337
4375
  return [...prefix, ...substring];
4338
4376
  }
4339
4377
 
4378
+ //#endregion
4379
+ //#region src/chat/tui/resume-hint.ts
4380
+ /**
4381
+ * The "Resume this conversation with: …" hint printed when the TUI exits.
4382
+ *
4383
+ * There are two ways out of the app — ChatScreen's quit (double ctrl+c or a
4384
+ * bare `exit`/`quit` line) and the app-root global ctrl+c on non-chat
4385
+ * screens (after `esc` back to the picker) — and both must print the hint:
4386
+ * esc'ing out of a live run then quitting is exactly the case where the id
4387
+ * is the only way back in (the agent keeps working server-side). So the
4388
+ * last-opened conversation is tracked here, module-scoped, and both
4389
+ * teardown sites call `printResumeHint()`.
4390
+ *
4391
+ * The write happens synchronously AFTER `renderer.destroy()` returns, never
4392
+ * on a process 'exit' hook — the native renderer teardown can end the
4393
+ * process without firing JS exit listeners (observed on darwin). By the
4394
+ * time destroy() returns, stdout is unhooked and the alt screen restored,
4395
+ * so the lines land in normal scrollback.
4396
+ */
4397
+ let lastConversationId = null;
4398
+ /** Record the conversation currently open in the chat screen (null while a
4399
+ * brand-new conversation hasn't been materialized on the server yet). */
4400
+ function setResumeConversation(conversationId) {
4401
+ lastConversationId = conversationId;
4402
+ }
4403
+ /** Print the resume hint for the last opened conversation, if any. Call
4404
+ * after `renderer.destroy()`. Prints at most once per process. */
4405
+ function printResumeHint(write = (text) => process.stdout.write(text)) {
4406
+ if (!lastConversationId) return;
4407
+ write(`\nResume this conversation with:\n skydive chat --resume ${lastConversationId}\n`);
4408
+ lastConversationId = null;
4409
+ }
4410
+
4340
4411
  //#endregion
4341
4412
  //#region src/chat/tui/screens/model-picker.tsx
4342
4413
  /**
@@ -5534,6 +5605,7 @@ function ChatScreen({ agent, conversation }) {
5534
5605
  }, [agentHost]);
5535
5606
  useEffect(() => {
5536
5607
  agentHost.setSession(conversationId);
5608
+ setResumeConversation(conversationId);
5537
5609
  }, [agentHost, conversationId]);
5538
5610
  const scrollRef = useRef(null);
5539
5611
  const composerRef = useRef(null);
@@ -6053,6 +6125,7 @@ function ChatScreen({ agent, conversation }) {
6053
6125
  portalClient?.dispose();
6054
6126
  agentHost.dispose();
6055
6127
  renderer.destroy();
6128
+ printResumeHint();
6056
6129
  }, [
6057
6130
  portalClient,
6058
6131
  agentHost,
@@ -6889,7 +6962,7 @@ function ToastView() {
6889
6962
 
6890
6963
  //#endregion
6891
6964
  //#region src/chat/tui/app.tsx
6892
- function App({ appUrl, sessionToken, shareMachine, promptHistoryPath, notifications, agentSelector }) {
6965
+ function App({ appUrl, sessionToken, shareMachine, promptHistoryPath, notifications, agentSelector, conversationId }) {
6893
6966
  const renderer = useRenderer();
6894
6967
  const screen = useStore((s) => s.screen);
6895
6968
  useStore((s) => s.themeId);
@@ -6961,7 +7034,8 @@ function App({ appUrl, sessionToken, shareMachine, promptHistoryPath, notificati
6961
7034
  try {
6962
7035
  const screen = await resolveInitialScreen({
6963
7036
  rest,
6964
- agentSelector
7037
+ agentSelector,
7038
+ conversationId
6965
7039
  });
6966
7040
  if (cancelled) return;
6967
7041
  goTo(screen);
@@ -6982,6 +7056,7 @@ function App({ appUrl, sessionToken, shareMachine, promptHistoryPath, notificati
6982
7056
  sessionToken,
6983
7057
  promptHistoryPath,
6984
7058
  agentSelector,
7059
+ conversationId,
6985
7060
  goTo,
6986
7061
  setClients,
6987
7062
  fail
@@ -6991,6 +7066,7 @@ function App({ appUrl, sessionToken, shareMachine, promptHistoryPath, notificati
6991
7066
  useStore.getState().portalClient?.dispose();
6992
7067
  agentHost.dispose();
6993
7068
  renderer.destroy();
7069
+ printResumeHint();
6994
7070
  }
6995
7071
  });
6996
7072
  return /* @__PURE__ */ jsx(AgentHostContext.Provider, {
@@ -7284,7 +7360,7 @@ var LinkifiedMarkdownRenderable = class extends MarkdownRenderable {
7284
7360
  * module — and the heavy OpenTUI dependency graph it pulls in — is
7285
7361
  * dynamically imported and never loaded on the Node management path.
7286
7362
  */
7287
- async function runChat({ appUrl, sessionToken, shareMachine, promptHistoryPath, theme: themeOverride, notifications, agentSelector }) {
7363
+ async function runChat({ appUrl, sessionToken, shareMachine, promptHistoryPath, theme: themeOverride, notifications, agentSelector, conversationId }) {
7288
7364
  const renderer = await createCliRenderer({ exitOnCtrlC: false });
7289
7365
  extend({ markdown: LinkifiedMarkdownRenderable });
7290
7366
  const store = useStore.getState();
@@ -7308,7 +7384,8 @@ async function runChat({ appUrl, sessionToken, shareMachine, promptHistoryPath,
7308
7384
  shareMachine,
7309
7385
  promptHistoryPath,
7310
7386
  notifications,
7311
- agentSelector: agentSelector ?? null
7387
+ agentSelector: agentSelector ?? null,
7388
+ conversationId: conversationId ?? null
7312
7389
  }));
7313
7390
  }
7314
7391
  /** Open the same workspace picker used by the initial agent screen. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skydive-cli",
3
- "version": "0.1.0-beta.341",
3
+ "version": "0.1.0-beta.347",
4
4
  "description": "Skydive CLI — manage AI agents from the command line",
5
5
  "homepage": "https://skydive.com",
6
6
  "license": "MIT",