@otto-code/cli 0.6.6 → 0.7.0

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.
@@ -9,7 +9,27 @@ export declare const deleteSchema: OutputSchema<DeleteResult>;
9
9
  export interface AgentDeleteOptions extends CommandOptions {
10
10
  all?: boolean;
11
11
  cwd?: string;
12
+ archived?: boolean;
13
+ includeArchived?: boolean;
12
14
  }
13
15
  export type AgentDeleteResult = SingleResult<DeleteResult>;
16
+ /** Which side of the archive line a bulk delete is allowed to touch. */
17
+ export type AgentDeleteScope = "active" | "archived" | "both";
18
+ /**
19
+ * Bulk delete used to filter `!a.archivedAt` unconditionally, so the one command
20
+ * that could clear the archive deliberately skipped exactly the rows a user most
21
+ * wants gone. The flags open it up without moving anyone's muscle memory: bare
22
+ * `--all` / `--cwd` still mean active-only.
23
+ *
24
+ * Both flags at once is contradictory ("only archived" and "also archived"), so
25
+ * it is refused rather than guessed — this command is irreversible.
26
+ */
27
+ export declare function resolveAgentDeleteScope(options: {
28
+ archived?: boolean;
29
+ includeArchived?: boolean;
30
+ }): AgentDeleteScope;
31
+ export declare function matchesAgentDeleteScope(agent: {
32
+ archivedAt?: Date | string | null;
33
+ }, scope: AgentDeleteScope): boolean;
14
34
  export declare function runDeleteCommand(id: string | undefined, options: AgentDeleteOptions, _command: Command): Promise<AgentDeleteResult>;
15
35
  //# sourceMappingURL=delete.d.ts.map
@@ -2,15 +2,50 @@ import { connectToDaemon, getDaemonHost } from "../../utils/client.js";
2
2
  import { isSameOrDescendantPath } from "../../utils/paths.js";
3
3
  export function addDeleteOptions(cmd) {
4
4
  return cmd
5
- .description("Delete an agent (interrupt if running, then hard-delete)")
5
+ .description("Delete an agent (interrupt if running, then hard-delete). Removes Otto's record only - the agent provider's own transcript is left on disk.")
6
6
  .argument("[id]", "Agent ID (or prefix) - optional if --all or --cwd specified")
7
7
  .option("--all", "Delete all agents")
8
- .option("--cwd <path>", "Delete all agents in directory");
8
+ .option("--cwd <path>", "Delete all agents in directory")
9
+ .option("--archived", "Only archived agents (use with --all or --cwd)")
10
+ .option("--include-archived", "Include archived agents (use with --all or --cwd)");
9
11
  }
10
12
  export const deleteSchema = {
11
13
  idField: (item) => item.agentIds.join("\n"),
12
14
  columns: [{ header: "DELETED", field: "deletedCount" }],
13
15
  };
16
+ /**
17
+ * Bulk delete used to filter `!a.archivedAt` unconditionally, so the one command
18
+ * that could clear the archive deliberately skipped exactly the rows a user most
19
+ * wants gone. The flags open it up without moving anyone's muscle memory: bare
20
+ * `--all` / `--cwd` still mean active-only.
21
+ *
22
+ * Both flags at once is contradictory ("only archived" and "also archived"), so
23
+ * it is refused rather than guessed — this command is irreversible.
24
+ */
25
+ export function resolveAgentDeleteScope(options) {
26
+ if (options.archived && options.includeArchived) {
27
+ const error = {
28
+ code: "CONFLICTING_OPTIONS",
29
+ message: "--archived and --include-archived cannot be combined",
30
+ details: "--archived deletes only archived agents; --include-archived deletes both",
31
+ };
32
+ throw error;
33
+ }
34
+ if (options.archived) {
35
+ return "archived";
36
+ }
37
+ if (options.includeArchived) {
38
+ return "both";
39
+ }
40
+ return "active";
41
+ }
42
+ export function matchesAgentDeleteScope(agent, scope) {
43
+ if (scope === "both") {
44
+ return true;
45
+ }
46
+ const isArchived = agent.archivedAt != null && agent.archivedAt !== "";
47
+ return scope === "archived" ? isArchived : !isArchived;
48
+ }
14
49
  export async function runDeleteCommand(id, options, _command) {
15
50
  const host = getDaemonHost({ host: options.host });
16
51
  if (!id && !options.all && !options.cwd) {
@@ -21,6 +56,9 @@ export async function runDeleteCommand(id, options, _command) {
21
56
  };
22
57
  throw error;
23
58
  }
59
+ // Resolved before connecting: a contradictory flag pair should fail fast, not
60
+ // after opening a socket.
61
+ const scope = resolveAgentDeleteScope(options);
24
62
  let client;
25
63
  try {
26
64
  client = await connectToDaemon({ host: options.host });
@@ -39,14 +77,10 @@ export async function runDeleteCommand(id, options, _command) {
39
77
  let agents = fetchPayload.entries.map((entry) => entry.agent);
40
78
  const deletedIds = [];
41
79
  if (options.all) {
42
- agents = agents.filter((a) => !a.archivedAt);
80
+ agents = agents.filter((a) => matchesAgentDeleteScope(a, scope));
43
81
  }
44
82
  else if (options.cwd) {
45
- agents = agents.filter((a) => {
46
- if (a.archivedAt)
47
- return false;
48
- return isSameOrDescendantPath(options.cwd, a.cwd);
49
- });
83
+ agents = agents.filter((a) => matchesAgentDeleteScope(a, scope) && isSameOrDescendantPath(options.cwd, a.cwd));
50
84
  }
51
85
  else if (id) {
52
86
  const fetchResult = await client.fetchAgent({ agentId: id });
@@ -6,7 +6,7 @@ import { resolve } from "node:path";
6
6
  import { lookup } from "mime-types";
7
7
  import { parseDuration } from "../../utils/duration.js";
8
8
  import { collectMultiple } from "../../utils/command-options.js";
9
- import { resolveProviderAndModel } from "../../utils/provider-model.js";
9
+ import { assertNoConflictingModelInputs, resolveProviderAndModel, } from "../../utils/provider-model.js";
10
10
  export { resolveProviderAndModel } from "../../utils/provider-model.js";
11
11
  export function addRunOptions(cmd) {
12
12
  return cmd
@@ -210,6 +210,10 @@ function structuredRunSchema(output) {
210
210
  };
211
211
  }
212
212
  function validateRunOptions(prompt, options, outputSchema) {
213
+ // Before anything opens a socket: a contradictory command line should say so, not report that
214
+ // the daemon is unreachable. Full provider/model resolution still happens after connecting,
215
+ // because it needs the host.
216
+ assertNoConflictingModelInputs(options.provider, options.model);
213
217
  if (!prompt || prompt.trim().length === 0) {
214
218
  throw {
215
219
  code: "MISSING_PROMPT",
@@ -13,6 +13,7 @@ export interface AgentSendOptions extends CommandOptions {
13
13
  image?: string[];
14
14
  prompt?: string;
15
15
  promptFile?: string;
16
+ queue?: boolean;
16
17
  }
17
18
  export declare function addSendOptions(cmd: Command): Command;
18
19
  export declare function runSendCommand(agentIdArg: string, prompt: string | undefined, options: AgentSendOptions, _command: Command): Promise<SingleResult<AgentSendResult>>;
@@ -19,7 +19,8 @@ export function addSendOptions(cmd) {
19
19
  .option("--prompt <text>", "Provide the message inline as a flag")
20
20
  .option("--prompt-file <path>", "Read the message from a UTF-8 text file")
21
21
  .option("--image <path>", "Attach image(s) to the message", collectMultiple, [])
22
- .option("--no-wait", "Return immediately without waiting for completion");
22
+ .option("--no-wait", "Return immediately without waiting for completion")
23
+ .option("--queue", "If the agent is busy, run this after its current turn instead of interrupting it");
23
24
  }
24
25
  /**
25
26
  * Read image files and convert them to base64 data URIs
@@ -148,7 +149,24 @@ export async function runSendCommand(agentIdArg, prompt, options, _command) {
148
149
  // Read image files if provided
149
150
  const images = options.image && options.image.length > 0 ? await readImageFiles(options.image) : undefined;
150
151
  // Send the message
151
- await client.sendAgentMessage(agentIdArg, promptInput, { images });
152
+ const dispatch = await client.sendAgentMessage(agentIdArg, promptInput, {
153
+ images,
154
+ ...(options.queue ? { delivery: "queue" } : {}),
155
+ });
156
+ // A queued message has no run to wait for yet — it starts when the current
157
+ // turn ends, so report it and return rather than blocking on --wait.
158
+ if (dispatch.queued) {
159
+ await client.close();
160
+ return {
161
+ type: "single",
162
+ data: {
163
+ agentId: agentIdArg,
164
+ status: "sent",
165
+ message: "Agent is busy; message queued to run as its next turn",
166
+ },
167
+ schema: agentSendSchema,
168
+ };
169
+ }
152
170
  // If --no-wait, return immediately
153
171
  if (options.wait === false) {
154
172
  await client.close();
@@ -7,5 +7,18 @@ export interface ResolvedProviderModel {
7
7
  provider: string;
8
8
  model: string | undefined;
9
9
  }
10
+ /**
11
+ * The one provider/model check that needs no daemon: `--provider a/b --model c` is contradictory
12
+ * on its face.
13
+ *
14
+ * Callers run this **before** connecting. Full resolution happens after a connection because it
15
+ * consults the host (personalities, defaults), so leaving this check there meant a plainly
16
+ * malformed command line reported "Cannot connect to daemon" instead of the real problem — and
17
+ * only on machines without a daemon running, which is why it read as a CI-only failure.
18
+ *
19
+ * Silent when either input is absent or they agree; `resolveProviderAndModel` still repeats the
20
+ * check for callers that reach it directly.
21
+ */
22
+ export declare function assertNoConflictingModelInputs(provider?: string, model?: string): void;
10
23
  export declare function resolveProviderAndModel(options: ResolveProviderAndModelOptions): ResolvedProviderModel;
11
24
  //# sourceMappingURL=provider-model.d.ts.map
@@ -1,3 +1,36 @@
1
+ /**
2
+ * The one provider/model check that needs no daemon: `--provider a/b --model c` is contradictory
3
+ * on its face.
4
+ *
5
+ * Callers run this **before** connecting. Full resolution happens after a connection because it
6
+ * consults the host (personalities, defaults), so leaving this check there meant a plainly
7
+ * malformed command line reported "Cannot connect to daemon" instead of the real problem — and
8
+ * only on machines without a daemon running, which is why it read as a CI-only failure.
9
+ *
10
+ * Silent when either input is absent or they agree; `resolveProviderAndModel` still repeats the
11
+ * check for callers that reach it directly.
12
+ */
13
+ export function assertNoConflictingModelInputs(provider, model) {
14
+ const providerInput = provider?.trim();
15
+ const modelInput = model?.trim();
16
+ if (!providerInput || !modelInput) {
17
+ return;
18
+ }
19
+ const slashIndex = providerInput.indexOf("/");
20
+ if (slashIndex === -1) {
21
+ return;
22
+ }
23
+ const modelFromProvider = providerInput.slice(slashIndex + 1).trim();
24
+ if (!modelFromProvider || modelFromProvider === modelInput) {
25
+ return;
26
+ }
27
+ const error = {
28
+ code: "CONFLICTING_MODEL_OPTIONS",
29
+ message: "Conflicting model values provided",
30
+ details: `--provider specifies model ${modelFromProvider}, but --model specifies ${modelInput}`,
31
+ };
32
+ throw error;
33
+ }
1
34
  export function resolveProviderAndModel(options) {
2
35
  const providerInput = options.provider?.trim() || options.defaultProvider;
3
36
  const modelInput = options.model?.trim();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@otto-code/cli",
3
- "version": "0.6.6",
3
+ "version": "0.7.0",
4
4
  "description": "Otto CLI - control your AI coding agents from the command line",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "bin": {
@@ -28,9 +28,9 @@
28
28
  },
29
29
  "dependencies": {
30
30
  "@clack/prompts": "^1.0.0",
31
- "@otto-code/client": "0.6.6",
32
- "@otto-code/protocol": "0.6.6",
33
- "@otto-code/server": "0.6.6",
31
+ "@otto-code/client": "0.7.0",
32
+ "@otto-code/protocol": "0.7.0",
33
+ "@otto-code/server": "0.7.0",
34
34
  "chalk": "^5.3.0",
35
35
  "commander": "^12.0.0",
36
36
  "mime-types": "^2.1.35",