@soft-boy/agentschema 0.1.1 → 0.1.2

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
@@ -1,6 +1,6 @@
1
1
  # agentschema
2
2
 
3
- Define a Claude Managed Agent as a folder of Markdown files.
3
+ Define an AI-Agent as a folder of Markdown files.
4
4
 
5
5
  ```
6
6
  my-agent/
@@ -8,7 +8,7 @@ my-agent/
8
8
  └── env.md # the environment it runs in
9
9
  ```
10
10
 
11
- **Status:** v0.1 draft. `check`, `deploy` and `run` work.
11
+ **Status:** v0.1 draft.
12
12
 
13
13
  ## Philosophy
14
14
 
@@ -22,35 +22,6 @@ my-agent/
22
22
  npm install -g @soft-boy/agentschema
23
23
  ```
24
24
 
25
- ## Example
26
-
27
- [`examples/coding-agent/agent.md`](examples/coding-agent/agent.md):
28
-
29
- ```markdown
30
- ---
31
- name: Coding Assistant
32
- model: claude-opus-5-5
33
- tools:
34
- - type: agent_toolset_20260401
35
- ---
36
-
37
- You are a helpful coding assistant. Write clean, well-documented code.
38
- ```
39
-
40
- [`examples/coding-agent/env.md`](examples/coding-agent/env.md):
41
-
42
- ```markdown
43
- ---
44
- name: quickstart
45
- config:
46
- type: cloud
47
- networking:
48
- type: limited
49
- allow_package_managers: true
50
- ---
51
- Cloud container with limited networking and package manager access.
52
- ```
53
-
54
25
  ## Specification
55
26
 
56
27
  ### Files
@@ -79,13 +50,26 @@ Field reference: [Environments](https://platform.claude.com/docs/en/managed-agen
79
50
  ## Usage
80
51
 
81
52
  ```sh
53
+ agentschema init <name> # scaffold a new agent folder
82
54
  agentschema check [dir] # validate the folder, no network
83
55
  agentschema deploy [dir] # create or update the agent and environment
84
56
  agentschema run [dir] # chat with the deployed agent in the terminal
57
+ agentschema list # list the agents in your workspace
58
+ agentschema delete <agent_id> # archive an agent
59
+ agentschema sessions list # list the sessions in your workspace
60
+ agentschema sessions delete <id> # delete a session
85
61
  ```
86
62
 
87
63
  `dir` defaults to the current directory.
88
64
 
65
+ **`init`** creates the folder `<name>` with an `agent.md` and an `env.md` that pass `check`. It asks for:
66
+ - the agent's name and model
67
+ - its system prompt
68
+ - whether it gets the built-in tools, and how tool calls are approved (`auto`, `always_allow` or `always_ask`)
69
+ - the environment's name and networking
70
+
71
+ Press Enter to keep the default shown, or pass `--yes` to skip the questions. `init` won't write into a folder that already has files in it.
72
+
89
73
  **`check`** reads both files and reports every problem it finds:
90
74
  - a missing file, or a name that isn't lowercase
91
75
  - frontmatter that is missing, unclosed, or invalid YAML
@@ -100,8 +84,6 @@ Unknown fields are only warnings, because they're sent to the API as written. `c
100
84
  - **The files are the source of truth.** A field you delete from a file is cleared on the next deploy, rather than left at its old value.
101
85
  - **Changes made elsewhere aren't overwritten silently.** If the agent was changed outside the folder since the last deploy, `deploy` stops; `--force` overwrites it.
102
86
 
103
- `deploy` authenticates with `ANTHROPIC_API_KEY`, or with a profile from `ant auth login`.
104
-
105
87
  `claude-lock.json` uses the same layout as `ant apply`'s, so `deploy` updates resources that `ant apply` created. The two tools hash files differently, though, so the first `deploy` after an `ant apply` updates both resources even if nothing changed.
106
88
 
107
89
  **`run`** starts a session on the agent and environment in `claude-lock.json`, then opens a chat in the terminal. Deploy first; if you've edited the files since, `run` warns you and still uses the deployed version.
@@ -109,16 +91,39 @@ Unknown fields are only warnings, because they're sent to the API as written. `c
109
91
  - **Approvals:** when a tool call needs approval (`always_ask`, or `auto` when the server can't decide), `run` asks you. Answer `y` to allow it, or type a reason to deny it; the agent sees the reason.
110
92
  - **Custom tools:** `run` shows the call's input and asks you to type the result.
111
93
  - **Interrupting:** Ctrl+C interrupts the agent mid-turn, and a second Ctrl+C quits. Ctrl+D quits at the prompt.
112
- - **Resuming:** quitting leaves the session in place, and `run` prints a command to resume it: `agentschema run --session sesn_…`.
94
+ - **Resuming:** quitting leaves the session in place, and `run` prints a command to resume it: `agentschema run --resume sesn_…`.
113
95
 
114
96
  ```sh
115
- agentschema run [dir] --vault vlt_… # attach a vault for MCP or other credentials (repeatable)
116
- agentschema run [dir] --title "Bug triage" # name the session
117
- agentschema run --session sesn_… # resume an existing session
97
+ agentschema run --resume sesn_… # resume an existing session
118
98
  ```
119
99
 
120
100
  To run the agent from your own code, create a session with the agent and environment IDs from the lock file.
121
101
 
102
+ **`list`** prints every agent in your workspace, with its ID, latest version, model and when it was last updated. It includes agents that weren't deployed with agentschema, and leaves out archived ones.
103
+
104
+ **`sessions list`** prints every session in your workspace, newest first, with its status, the agent and version it runs, and its title. Pass an ID to `run --resume` to pick a session back up.
105
+
106
+ **`delete`** archives an agent. The API can't delete agents outright, so this is as close as it gets. An archived agent drops out of `list`, and the API has no way to unarchive it. If the agent is in a folder's `claude-lock.json`, remove its entry so the next `deploy` creates a new one.
107
+
108
+ **`sessions delete`** permanently deletes a session and its event history. A running session can't be deleted; wait for it to go idle, or interrupt it, first.
109
+
110
+ Both ask for confirmation first, showing the agent's name or the session's title. Pass `--yes` to skip that, for example in scripts; without a terminal to ask in, they refuse unless you pass `--yes`.
111
+
112
+ ### Authentication
113
+
114
+ agentschema has no account setting of its own. Every command except `init` and `check` uses whichever Anthropic credentials are in your shell:
115
+
116
+ 1. `ANTHROPIC_API_KEY`, if it's set
117
+ 2. otherwise, the profile you signed in to with `ant auth login` (the active one, or the one `ANTHROPIC_PROFILE` names)
118
+
119
+ An API key belongs to one workspace, and the agent and environment are created in that workspace. Each person deploys with their own key, and to deploy somewhere else you switch the key:
120
+
121
+ ```sh
122
+ ANTHROPIC_API_KEY=sk-ant-… agentschema deploy my-agent
123
+ ```
124
+
125
+ `claude-lock.json` holds the IDs from one workspace. A committed lock works for everyone whose key is in that workspace, and they all update the same agent. If you deploy the folder from a key in a different workspace, `deploy` can't find those IDs and stops. In that case, deploy from a copy of the folder, or don't commit that lock file.
126
+
122
127
  ## Development
123
128
 
124
129
  ```sh
@@ -1,44 +1,58 @@
1
1
  #!/usr/bin/env node
2
2
  import { readFileSync } from "node:fs";
3
+ import { createInterface } from "node:readline/promises";
3
4
  import { parseArgs } from "node:util";
4
5
  import Anthropic from "@anthropic-ai/sdk";
5
6
  import { chat } from "../src/chat.js";
6
7
  import { check } from "../src/check.js";
8
+ import { DeleteError, deleteAgent, deleteSession } from "../src/delete.js";
7
9
  import { DeployError, deploy } from "../src/deploy.js";
10
+ import { InitError, init } from "../src/init.js";
11
+ import { listAgents, listSessions } from "../src/list.js";
8
12
  import { SessionError, openSession } from "../src/session.js";
9
13
 
10
14
  const USAGE = `Usage: agentschema <command> [dir] [options]
11
15
 
12
16
  Commands:
13
- check [dir] Validate agent.md and env.md, without calling the API
14
- deploy [dir] Create or update the agent and environment, and write claude-lock.json
15
- run [dir] Chat with the deployed agent in a new session
17
+ init <name> Scaffold a new agent folder, asking a few questions
18
+ check [dir] Validate agent.md and env.md, without calling the API
19
+ deploy [dir] Create or update the agent and environment, and write claude-lock.json
20
+ run [dir] Chat with the deployed agent in a new session
21
+ list List the agents in your workspace
22
+ delete <agent_id> Archive an agent (agents can't be deleted outright)
23
+ sessions list List the sessions in your workspace, newest first
24
+ sessions delete <id> Delete a session and its history
16
25
 
17
26
  Options:
27
+ -y, --yes (init) Skip the questions and use the defaults
28
+ (delete) Don't ask for confirmation
18
29
  --dry-run (deploy) Show what would be created or updated, change nothing
19
30
  --force (deploy) Update the agent even if it was changed outside this folder
20
- --vault <id> (run) Attach a vault (vlt_...) for MCP and other credentials; repeatable
21
- --title <title> (run) Title for the new session
22
- --session <id> (run) Resume an existing session (sesn_...) instead of starting one
31
+ --resume <id> (run) Resume an existing session (sesn_...) instead of starting one
23
32
  -h, --help Show this help
24
33
  -v, --version Show the version
25
34
 
26
- dir defaults to the current directory. deploy and run authenticate with
35
+ dir defaults to the current directory. Every command but init and check authenticates with
27
36
  ANTHROPIC_API_KEY, or with a profile from \`ant auth login\`.`;
28
37
 
29
38
  const OPTIONS = {
39
+ yes: { type: "boolean", short: "y" },
30
40
  "dry-run": { type: "boolean" },
31
41
  force: { type: "boolean" },
32
- vault: { type: "string", multiple: true },
33
- title: { type: "string" },
34
- session: { type: "string" },
42
+ resume: { type: "string" },
35
43
  help: { type: "boolean", short: "h" },
36
44
  version: { type: "boolean", short: "v" },
37
45
  };
38
- const COMMAND_OPTIONS = {
39
- check: [],
40
- deploy: ["dry-run", "force"],
41
- run: ["vault", "title", "session"],
46
+ // For each command: how many positional arguments it takes, and which options.
47
+ const COMMANDS = {
48
+ init: { args: [1, 1], options: ["yes"] },
49
+ check: { args: [0, 1], options: [] },
50
+ deploy: { args: [0, 1], options: ["dry-run", "force"] },
51
+ run: { args: [0, 1], options: ["resume"] },
52
+ list: { args: [0, 0], options: [] },
53
+ delete: { args: [1, 1], options: ["yes"] },
54
+ "sessions list": { args: [0, 0], options: [] },
55
+ "sessions delete": { args: [1, 1], options: ["yes"] },
42
56
  };
43
57
 
44
58
  async function main(argv) {
@@ -50,7 +64,8 @@ async function main(argv) {
50
64
  return 1;
51
65
  }
52
66
  const { values, positionals } = parsed;
53
- const [command, dir = ".", ...extra] = positionals;
67
+ let [command, ...args] = positionals;
68
+ if (command === "sessions") command = `sessions ${args.shift() ?? ""}`.trim();
54
69
 
55
70
  if (values.version) {
56
71
  const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
@@ -61,16 +76,38 @@ async function main(argv) {
61
76
  console.log(USAGE);
62
77
  return command || values.help ? 0 : 1;
63
78
  }
64
- if (!(command in COMMAND_OPTIONS)) {
79
+ if (command.startsWith("sessions") && !(command in COMMANDS)) {
80
+ console.error(`agentschema: usage: agentschema sessions list | sessions delete <id>`);
81
+ return 1;
82
+ }
83
+ if (!(command in COMMANDS)) {
65
84
  console.error(`agentschema: unknown command "${command}"\n\n${USAGE}`);
66
85
  return 1;
67
86
  }
68
- const misplaced = Object.keys(values).filter((k) => !COMMAND_OPTIONS[command].includes(k));
69
- if (extra.length > 0 || misplaced.length > 0) {
70
- const arg = extra[0] ?? `--${misplaced[0]}`;
87
+ const spec = COMMANDS[command];
88
+ const misplaced = Object.keys(values).filter((k) => !spec.options.includes(k));
89
+ if (args.length > spec.args[1] || misplaced.length > 0) {
90
+ const arg = args[spec.args[1]] ?? `--${misplaced[0]}`;
71
91
  console.error(`agentschema: unexpected argument ${arg} for ${command}\n\n${USAGE}`);
72
92
  return 1;
73
93
  }
94
+ if (args.length < spec.args[0]) {
95
+ const example = { init: "init my-agent", delete: "delete agent_…", "sessions delete": "sessions delete sesn_…" }[command];
96
+ console.error(`agentschema: ${command} needs an argument, e.g. agentschema ${example}`);
97
+ return 1;
98
+ }
99
+ const dir = args[0] ?? ".";
100
+
101
+ if (command === "init") {
102
+ try {
103
+ await init(dir, { yes: values.yes ?? false });
104
+ return 0;
105
+ } catch (err) {
106
+ if (!(err instanceof InitError)) throw err;
107
+ console.error(`error: ${err.message}`);
108
+ return 1;
109
+ }
110
+ }
74
111
 
75
112
  if (command === "check") {
76
113
  const { errors, warnings } = check(dir);
@@ -94,13 +131,26 @@ async function main(argv) {
94
131
  return 0;
95
132
  }
96
133
 
134
+ if (command === "list" || command === "sessions list") {
135
+ const lines = command === "list" ? await listAgents(makeClient(false)) : await listSessions(makeClient(false));
136
+ for (const line of lines) console.log(line);
137
+ return 0;
138
+ }
139
+
140
+ if (command === "delete" || command === "sessions delete") {
141
+ const remove = command === "delete" ? deleteAgent : deleteSession;
142
+ await remove(makeClient(false), args[0], {
143
+ confirm: values.yes ? async () => true : confirm,
144
+ log: (line) => console.log(line),
145
+ });
146
+ return 0;
147
+ }
148
+
97
149
  if (command === "run") {
98
150
  const client = makeClient(false);
99
151
  const { session, notes } = await openSession(dir, {
100
152
  client,
101
- sessionId: values.session,
102
- vaultIds: values.vault ?? [],
103
- title: values.title,
153
+ sessionId: values.resume,
104
154
  });
105
155
  for (const note of notes) console.warn(`warning: ${note}`);
106
156
  await chat(client, session, { agentName: session.agent?.name ?? "agent" });
@@ -111,7 +161,7 @@ async function main(argv) {
111
161
  console.error(`error: ${err.status ?? "network error"}: ${err.error?.error?.message ?? err.message}`);
112
162
  return 1;
113
163
  }
114
- if (err instanceof DeployError || err instanceof SessionError || err instanceof Anthropic.AnthropicError) {
164
+ if (err instanceof DeployError || err instanceof DeleteError || err instanceof SessionError || err instanceof Anthropic.AnthropicError) {
115
165
  console.error(`error: ${err.message}`);
116
166
  return 1;
117
167
  }
@@ -119,6 +169,17 @@ async function main(argv) {
119
169
  }
120
170
  }
121
171
 
172
+ // Asks a yes/no question in the terminal; without one, there's no one to ask.
173
+ async function confirm(question) {
174
+ if (!process.stdin.isTTY) throw new DeleteError("not a terminal, so can't ask for confirmation; pass --yes");
175
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
176
+ try {
177
+ return /^(y|yes)$/i.test((await rl.question(`${question} [y/N] `)).trim());
178
+ } finally {
179
+ rl.close();
180
+ }
181
+ }
182
+
122
183
  // A dry run makes no API calls, so it works without credentials.
123
184
  function makeClient(dryRun) {
124
185
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@soft-boy/agentschema",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Markdown based AI agent schema",
5
5
  "type": "module",
6
6
  "bin": {
package/src/chat.js CHANGED
@@ -131,5 +131,5 @@ export async function chat(client, session, { agentName, input = process.stdin,
131
131
  conversation.close();
132
132
  rl.close();
133
133
  }
134
- if (!ended) output.write(dim(`Resume this conversation with: agentschema run --session ${session.id}\n`));
134
+ if (!ended) output.write(dim(`Resume this conversation with: agentschema run --resume ${session.id}\n`));
135
135
  }
package/src/check.js CHANGED
@@ -27,7 +27,7 @@ export function check(dir) {
27
27
  for (const [file, parsed] of [[AGENT_FILE, agent], [ENV_FILE, env]]) {
28
28
  if (!parsed) continue;
29
29
  for (const [pattern, what] of SECRET_PATTERNS) {
30
- if (pattern.test(parsed.raw)) errors.push(`${file}: looks like it contains ${what}; keep secrets in a vault`);
30
+ if (pattern.test(parsed.raw)) errors.push(`${file}: looks like it contains ${what}; keep secrets out of these files`);
31
31
  }
32
32
  }
33
33
 
package/src/delete.js ADDED
@@ -0,0 +1,25 @@
1
+ export class DeleteError extends Error {}
2
+
3
+ // Agents can't be deleted, only archived: an archived agent drops out of `list`, and
4
+ // there's no unarchive. `confirm(question)` resolves to true to go ahead.
5
+ export async function deleteAgent(client, agentId, { confirm, log }) {
6
+ const agent = await client.beta.agents.retrieve(agentId);
7
+ if (agent.archived_at) throw new DeleteError(`${agentId} is already archived`);
8
+ const ok = await confirm(`Archive agent "${agent.name}" (${agentId})? It can't be undone.`);
9
+ if (!ok) return log("Nothing changed.");
10
+ await client.beta.agents.archive(agentId);
11
+ log(`Archived ${agentId}.`);
12
+ }
13
+
14
+ // Deletes a session and its event history for good.
15
+ export async function deleteSession(client, sessionId, { confirm, log }) {
16
+ const session = await client.beta.sessions.retrieve(sessionId);
17
+ if (session.status === "running") {
18
+ throw new DeleteError(`${sessionId} is running; wait for it to go idle, or interrupt it, then try again`);
19
+ }
20
+ const label = session.title ? `"${session.title}" (${sessionId})` : sessionId;
21
+ const ok = await confirm(`Delete session ${label} and its history? It can't be undone.`);
22
+ if (!ok) return log("Nothing changed.");
23
+ await client.beta.sessions.delete(sessionId);
24
+ log(`Deleted ${sessionId}.`);
25
+ }
package/src/init.js ADDED
@@ -0,0 +1,186 @@
1
+ import { existsSync, mkdirSync, readdirSync, writeFileSync } from "node:fs";
2
+ import { basename, resolve } from "node:path";
3
+ import { createInterface, emitKeypressEvents } from "node:readline";
4
+ import { stringify } from "yaml";
5
+ import { AGENT_FILE, ENV_FILE } from "./instance.js";
6
+
7
+ export class InitError extends Error {}
8
+
9
+ export const MODELS = ["claude-opus-5-5", "claude-sonnet-5-5", "claude-haiku-5-5"];
10
+ export const APPROVALS = ["auto", "always_allow", "always_ask"];
11
+ export const NETWORKING = ["limited", "unrestricted"];
12
+
13
+ // The answers used when a question is skipped (--yes, or input ends early).
14
+ export function defaults(dir) {
15
+ const slug = basename(resolve(dir)).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "agent";
16
+ return {
17
+ name: slug.split("-").map((w) => w[0].toUpperCase() + w.slice(1)).join(" "),
18
+ model: MODELS[0],
19
+ system: "You are a helpful assistant.",
20
+ tools: true,
21
+ approval: "auto",
22
+ environment: slug,
23
+ networking: "limited",
24
+ packageManagers: true,
25
+ };
26
+ }
27
+
28
+ // Returns { "agent.md": text, "env.md": text } for a set of answers.
29
+ export function scaffold(answers) {
30
+ const agent = { name: answers.name, model: answers.model };
31
+ if (answers.tools) {
32
+ const toolset = { type: "agent_toolset_20260401" };
33
+ // always_allow is the toolset's default, so it needs no config.
34
+ if (answers.approval !== "always_allow") toolset.default_config = { permission_policy: { type: answers.approval } };
35
+ agent.tools = [toolset];
36
+ }
37
+
38
+ const networking = { type: answers.networking };
39
+ if (answers.networking === "limited") networking.allow_package_managers = answers.packageManagers;
40
+ const env = { name: answers.environment, config: { type: "cloud", networking } };
41
+
42
+ const access = answers.networking === "unrestricted"
43
+ ? "unrestricted network access"
44
+ : answers.packageManagers ? "limited networking and package manager access" : "limited networking";
45
+ return {
46
+ [AGENT_FILE]: `---\n${stringify(agent)}---\n\n${answers.system.trim()}\n`,
47
+ [ENV_FILE]: `---\n${stringify(env)}---\nCloud container with ${access}.\n`,
48
+ };
49
+ }
50
+
51
+ // Asks the setup questions, then writes agent.md and env.md into a new folder `dir`.
52
+ export async function init(dir, { yes = false, input = process.stdin, output = process.stdout } = {}) {
53
+ if (existsSync(dir) && readdirSync(dir).length > 0) {
54
+ throw new InitError(`${dir} already exists and isn't empty`);
55
+ }
56
+
57
+ const answers = defaults(dir);
58
+ if (!yes) await ask(answers, input, output);
59
+
60
+ mkdirSync(dir, { recursive: true });
61
+ const files = scaffold(answers);
62
+ for (const [file, text] of Object.entries(files)) writeFileSync(resolve(dir, file), text);
63
+
64
+ output.write(`\nCreated ${dir}/${AGENT_FILE} and ${dir}/${ENV_FILE}.\n\nNext:\n`);
65
+ output.write(` agentschema deploy ${dir}\n agentschema run ${dir}\n`);
66
+ return files;
67
+ }
68
+
69
+ // Fills in `answers` from the user's replies; an empty reply keeps the default shown.
70
+ async function ask(answers, input, output) {
71
+ const io = input.isTTY ? terminalPrompts(input, output) : linePrompts(input, output);
72
+ const confirm = async (question, fallback) => {
73
+ while (true) {
74
+ const reply = (await io.text(`${question} [y/n]`, fallback ? "y" : "n")).toLowerCase();
75
+ if (/^(y|yes)$/.test(reply)) return true;
76
+ if (/^(n|no)$/.test(reply)) return false;
77
+ output.write("Answer y or n\n");
78
+ }
79
+ };
80
+
81
+ try {
82
+ answers.name = await io.text("Agent name?", answers.name);
83
+ answers.model = await io.select("Model?", MODELS, answers.model);
84
+ answers.system = await io.text("System prompt?", answers.system);
85
+ answers.tools = await confirm("Give it the built-in tools (bash, files, web)?", answers.tools);
86
+ if (answers.tools) answers.approval = await io.select("Tool approvals?", APPROVALS, answers.approval);
87
+ answers.environment = await io.text("Environment name?", answers.environment);
88
+ answers.networking = await io.select("Networking?", NETWORKING, answers.networking);
89
+ if (answers.networking === "limited") {
90
+ answers.packageManagers = await confirm("Allow package managers (npm, pip, …)?", answers.packageManagers);
91
+ }
92
+ } finally {
93
+ io.close();
94
+ }
95
+ }
96
+
97
+ // Prompts for piped input: one answer per line, and choices picked by name or number.
98
+ function linePrompts(input, output) {
99
+ const rl = createInterface({ input, output, terminal: false });
100
+ const lines = rl[Symbol.asyncIterator]();
101
+ let ended = false;
102
+
103
+ const text = async (question, fallback) => {
104
+ if (ended) return fallback;
105
+ output.write(`${question} (${fallback}) `);
106
+ const { value, done } = await lines.next();
107
+ if (done) {
108
+ ended = true;
109
+ output.write("\n");
110
+ return fallback;
111
+ }
112
+ return value.trim() || fallback;
113
+ };
114
+ const select = async (question, choices, fallback) => {
115
+ const listed = choices.map((c, i) => `${i + 1}) ${c}`).join(" ");
116
+ while (true) {
117
+ const reply = await text(`${question} ${listed}`, String(choices.indexOf(fallback) + 1));
118
+ const picked = choices[Number(reply) - 1] ?? choices.find((c) => c === reply);
119
+ if (picked) return picked;
120
+ output.write(`Pick one of: ${choices.join(", ")}\n`);
121
+ }
122
+ };
123
+ return { text, select, close: () => rl.close() };
124
+ }
125
+
126
+ const ESC = "\x1b[";
127
+ const cyan = (s) => `${ESC}36m${s}${ESC}0m`;
128
+ const dim = (s) => `${ESC}2m${s}${ESC}0m`;
129
+
130
+ // Prompts for a terminal: choices are picked with the arrow keys and Enter.
131
+ function terminalPrompts(input, output) {
132
+ const cancelled = () => new InitError("cancelled");
133
+
134
+ // A fresh readline per question, so it doesn't fight the raw-mode select for the arrow keys.
135
+ const text = (question, fallback) =>
136
+ new Promise((resolve, reject) => {
137
+ const rl = createInterface({ input, output, terminal: true });
138
+ rl.on("SIGINT", () => {
139
+ rl.close();
140
+ output.write("\n");
141
+ reject(cancelled());
142
+ });
143
+ rl.question(`${question} ${dim(`(${fallback})`)} `, (reply) => {
144
+ rl.close();
145
+ resolve(reply.trim() || fallback);
146
+ });
147
+ });
148
+
149
+ const select = (question, choices, fallback) =>
150
+ new Promise((resolve, reject) => {
151
+ let index = Math.max(0, choices.indexOf(fallback));
152
+ const render = (first) => {
153
+ if (!first) output.write(`${ESC}${choices.length}A`);
154
+ for (const [i, choice] of choices.entries()) {
155
+ output.write(`${ESC}2K${i === index ? cyan(`❯ ${choice}`) : ` ${choice}`}\n`);
156
+ }
157
+ };
158
+ const finish = (err) => {
159
+ input.off("keypress", onKey);
160
+ input.setRawMode(false);
161
+ input.pause();
162
+ // Collapse the list to one line with the answer.
163
+ output.write(`${ESC}${choices.length + 1}A${ESC}J${ESC}?25h`);
164
+ output.write(`${question} ${err ? dim("cancelled") : cyan(choices[index])}\n`);
165
+ err ? reject(err) : resolve(choices[index]);
166
+ };
167
+ const onKey = (str, key = {}) => {
168
+ if (key.ctrl && key.name === "c") return finish(cancelled());
169
+ if (key.name === "return" || key.name === "enter") return finish();
170
+ if (key.name === "up" || key.name === "k") index = (index - 1 + choices.length) % choices.length;
171
+ else if (key.name === "down" || key.name === "j" || key.name === "tab") index = (index + 1) % choices.length;
172
+ else if (/^[1-9]$/.test(str ?? "") && Number(str) <= choices.length) index = Number(str) - 1;
173
+ else return;
174
+ render(false);
175
+ };
176
+
177
+ emitKeypressEvents(input);
178
+ input.setRawMode(true);
179
+ input.resume();
180
+ output.write(`${question} ${dim("(↑/↓, Enter)")}\n${ESC}?25l`);
181
+ render(true);
182
+ input.on("keypress", onKey);
183
+ });
184
+
185
+ return { text, select, close() {} };
186
+ }
package/src/list.js ADDED
@@ -0,0 +1,49 @@
1
+ // Lists the agents and sessions in the workspace the credentials belong to.
2
+
3
+ export async function listAgents(client) {
4
+ const rows = [];
5
+ for await (const agent of client.beta.agents.list()) {
6
+ rows.push({
7
+ name: agent.name,
8
+ id: agent.id,
9
+ version: String(agent.version),
10
+ model: agent.model?.id ?? "",
11
+ updated: formatDate(agent.updated_at),
12
+ });
13
+ }
14
+ return table(rows, { name: "NAME", id: "ID", version: "VERSION", model: "MODEL", updated: "UPDATED" }, "No agents.");
15
+ }
16
+
17
+ export async function listSessions(client) {
18
+ const rows = [];
19
+ // Newest first, the API's default order.
20
+ for await (const session of client.beta.sessions.list()) {
21
+ rows.push({
22
+ id: session.id,
23
+ status: session.status,
24
+ agent: session.agent ? `${session.agent.name} v${session.agent.version}` : "",
25
+ title: truncate(session.title ?? "", 40),
26
+ created: formatDate(session.created_at),
27
+ });
28
+ }
29
+ return table(rows, { id: "ID", status: "STATUS", agent: "AGENT", title: "TITLE", created: "CREATED" }, "No sessions.");
30
+ }
31
+
32
+ // Lines of a plain-text table, with columns padded to their widest value.
33
+ function table(rows, headers, empty) {
34
+ if (rows.length === 0) return [empty];
35
+ const keys = Object.keys(headers);
36
+ const widths = keys.map((k) => Math.max(headers[k].length, ...rows.map((r) => r[k].length)));
37
+ const line = (r) => keys.map((k, i) => (i === keys.length - 1 ? r[k] : r[k].padEnd(widths[i]))).join(" ").trimEnd();
38
+ return [line(headers), ...rows.map(line)];
39
+ }
40
+
41
+ // "2026-11-03 14:02" in local time.
42
+ function formatDate(iso) {
43
+ if (!iso) return "";
44
+ const d = new Date(iso);
45
+ const pad = (n) => String(n).padStart(2, "0");
46
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
47
+ }
48
+
49
+ const truncate = (s, n) => (s.length > n ? `${s.slice(0, n - 1)}…` : s);
package/src/session.js CHANGED
@@ -6,7 +6,7 @@ export class SessionError extends Error {}
6
6
 
7
7
  // Starts a session on the agent and environment recorded in claude-lock.json, or resumes `sessionId`.
8
8
  // Returns { session, notes }: notes are warnings worth showing, such as files changed since the last deploy.
9
- export async function openSession(dir, { client, sessionId, vaultIds = [], title }) {
9
+ export async function openSession(dir, { client, sessionId }) {
10
10
  if (sessionId) {
11
11
  const session = await client.beta.sessions.retrieve(sessionId);
12
12
  if (session.status === "terminated") throw new SessionError(`session ${sessionId} has terminated`);
@@ -33,9 +33,8 @@ export async function openSession(dir, { client, sessionId, vaultIds = [], title
33
33
  agent: { type: "agent", id: agent.id, version: Number(agent.version) },
34
34
  environment_id: env.id,
35
35
  };
36
- if (vaultIds.length > 0) params.vault_ids = vaultIds;
37
36
  const name = instance?.agent.data.name;
38
- params.title = title ?? (name ? `${name} (agentschema run)` : "agentschema run");
37
+ params.title = name ? `${name} (agentschema run)` : "agentschema run";
39
38
  return { session: await client.beta.sessions.create(params), notes };
40
39
  }
41
40