@shardflux/cli 0.24.0 → 0.24.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.
Files changed (49) hide show
  1. package/CHANGELOG.md +119 -0
  2. package/README.md +227 -1
  3. package/dist/account-args.js +5 -0
  4. package/dist/account-pool.js +5 -1
  5. package/dist/acp-permission-elsewhere.d.ts +38 -0
  6. package/dist/acp-permission-elsewhere.js +85 -0
  7. package/dist/acp-rpc.d.ts +2 -1
  8. package/dist/acp-rpc.js +11 -2
  9. package/dist/acp.js +95 -22
  10. package/dist/agent-credentials.d.ts +3 -0
  11. package/dist/agent-credentials.js +7 -0
  12. package/dist/args.d.ts +1 -1
  13. package/dist/args.js +143 -8
  14. package/dist/board-args.d.ts +6 -0
  15. package/dist/board-args.js +155 -0
  16. package/dist/board-commands.d.ts +62 -0
  17. package/dist/board-commands.js +395 -0
  18. package/dist/codex-cloud.d.ts +30 -3
  19. package/dist/codex-cloud.js +86 -12
  20. package/dist/conversation-commands.d.ts +30 -0
  21. package/dist/conversation-commands.js +312 -9
  22. package/dist/conversation-transcript.d.ts +44 -0
  23. package/dist/conversation-transcript.js +206 -0
  24. package/dist/conversations.d.ts +5 -2
  25. package/dist/conversations.js +20 -10
  26. package/dist/device-approvals.js +4 -3
  27. package/dist/device-bridge.d.ts +42 -1
  28. package/dist/device-bridge.js +223 -12
  29. package/dist/device-folders.d.ts +58 -0
  30. package/dist/device-folders.js +228 -0
  31. package/dist/device-helper.d.ts +6 -0
  32. package/dist/device-helper.js +87 -14
  33. package/dist/drive-commands.d.ts +25 -0
  34. package/dist/drive-commands.js +201 -0
  35. package/dist/handlers.js +6 -1
  36. package/dist/main.js +3 -0
  37. package/dist/move-files.d.ts +39 -0
  38. package/dist/move-files.js +167 -0
  39. package/dist/move.js +12 -11
  40. package/dist/onboarding.d.ts +6 -2
  41. package/dist/onboarding.js +24 -6
  42. package/dist/project-git.d.ts +7 -1
  43. package/dist/project-git.js +16 -3
  44. package/dist/project-preparation.d.ts +7 -0
  45. package/dist/project-preparation.js +14 -0
  46. package/dist/project-template-setup.js +32 -11
  47. package/dist/router-commands.d.ts +22 -0
  48. package/dist/router-commands.js +179 -0
  49. package/package.json +2 -2
@@ -0,0 +1,155 @@
1
+ const PROJECT_FILTER = { project: { type: 'string', value: '<id>', description: 'Only this project.' } };
2
+ const PROJECT = { project: { type: 'string', value: '<id>', description: 'The project (default: the API key’s).' } };
3
+ const BUCKET_FLAGS = (verb) => ({
4
+ now: { type: 'boolean', description: `${verb} Now.` },
5
+ next: { type: 'boolean', description: `${verb} Next.` },
6
+ later: { type: 'boolean', description: `${verb} Later.` },
7
+ });
8
+ const ORDER = {
9
+ after: { type: 'string', value: '<task-id>', description: 'Place it right after this task of its bucket.' },
10
+ first: { type: 'boolean', description: 'Place it first in its bucket.' },
11
+ };
12
+ const MODES = 'ask sends every permission request to you (shard needs-you); routine approves tests, builds, reads and installs for you; trusted approves everything except deploys, pushes to main and spending.';
13
+ export const BOARD_COMMANDS = [
14
+ {
15
+ path: ['board'],
16
+ summary: 'Show the board: what needs you, then every task in Now, Next and Later (0.25.0+)',
17
+ positionals: [],
18
+ options: { ...PROJECT_FILTER, follow: { type: 'boolean', short: 'f', description: 'Keep printing each change as it happens, until Ctrl-C.' } },
19
+ description: [
20
+ 'One board on every device: what waits on you comes first, oldest first, each with its id (shard needs-you answer',
21
+ 'takes it) and what is asked. Then the tasks of Now, Next and Later in order: id, state (queued, working, needs_you,',
22
+ 'done, unverified, failed), title or your words, and project. Reading the board never wakes a computer.',
23
+ '',
24
+ '--follow keeps printing changes from the held feed: a changed task with its bucket (or deleted, cleared), a sealed',
25
+ 'report with its evidence, what started or stopped waiting on you, the approval policy. --json prints the board;',
26
+ 'with --follow, the board on the first line, then one feed answer per line.',
27
+ ].join('\n'),
28
+ examples: ['shard board', 'shard board --follow', `shard board --json | jq -r '.needs_you[] | "\\(.id)\\t\\(.ask)"'`],
29
+ },
30
+ {
31
+ path: ['task', 'add'],
32
+ summary: 'Add a task in your own words (0.25.0+)',
33
+ positionals: [{ name: 'words', variadic: true, words: true, required: true }],
34
+ options: {
35
+ ...PROJECT,
36
+ next: { type: 'boolean', description: 'Add it to Next (default: Now).' },
37
+ later: { type: 'boolean', description: 'Add it to Later.' },
38
+ home: { type: 'boolean', description: 'A personal task: it runs on the project’s machine home, without a repository.' },
39
+ after: { type: 'string', value: '<task-id>', description: 'Place it right after this task of its bucket (default: last).' },
40
+ first: { type: 'boolean', description: 'Place it first in its bucket.' },
41
+ },
42
+ description: 'Your words are kept exactly as you wrote them. The project’s master agent picks the task up and starts the work; its reports arrive on the board (shard task show). Prints the task id, its bucket and its state.',
43
+ examples: ['shard task add Fix the wake race in resume', 'shard task add --later "Try a shorter pricing headline"', 'shard task add --first Hotfix the login redirect'],
44
+ },
45
+ {
46
+ path: ['task', 'show'],
47
+ summary: 'Show a task, its plan and every report sealed for it (0.25.0+)',
48
+ positionals: [{ name: 'id', required: true }],
49
+ options: {},
50
+ description: 'Prints its title or your words, its state, bucket and project, your exact words, the plan with each step’s state, the conversations that work on it, then each report oldest first: milestone, title, outcome and evidence (tests 12/12, live probe 200, screenshot) with the command, URL or file behind each. --json returns {task, reports}.',
51
+ examples: ['shard task show task_h7g4d7tqyv2pz3a2npyo4rpgzq', 'shard task show task_h7g4d7tqyv2pz3a2npyo4rpgzq --json | jq ".reports[-1].evidence"'],
52
+ },
53
+ {
54
+ path: ['task', 'edit'],
55
+ summary: 'Edit, move, keep or clear a task (0.25.0+)',
56
+ positionals: [{ name: 'id', required: true }],
57
+ options: {
58
+ text: { type: 'string', value: '<words>', description: 'New words; its master hears the change.' },
59
+ title: { type: 'string', value: '<title>', description: 'A short title ("" removes it).' },
60
+ ...BUCKET_FLAGS('Move it to'),
61
+ ...ORDER,
62
+ keep: { type: 'boolean', description: 'Keep an agent’s suggestion as a task of your own.' },
63
+ clear: { type: 'boolean', description: 'Clear it from the board (--no-clear brings it back).' },
64
+ },
65
+ description: 'Moving a task (--now, --next, --later, --after, --first) sets the order its master works in. Prints the task as shard board lists it.',
66
+ examples: ['shard task edit task_h7g4d7tqyv2pz3a2npyo4rpgzq --first', 'shard task edit task_h7g4d7tqyv2pz3a2npyo4rpgzq --next --title "Wake race"', 'shard task edit task_h7g4d7tqyv2pz3a2npyo4rpgzq --keep'],
67
+ },
68
+ {
69
+ path: ['task', 'rm'],
70
+ summary: 'Delete a task; its master stops the work (0.25.0+)',
71
+ positionals: [{ name: 'id', required: true }],
72
+ options: {},
73
+ description: 'Its follow-ups go with it, on every device.',
74
+ },
75
+ {
76
+ path: ['task', 'steer'],
77
+ summary: 'Send steering words to a task’s master (0.25.0+)',
78
+ positionals: [{ name: 'id', required: true }, { name: 'words', variadic: true, words: true, required: true }],
79
+ options: {},
80
+ description: 'Your words go to the agent that runs the task, which adjusts its plan. The task keeps its own words.',
81
+ examples: ['shard task steer task_h7g4d7tqyv2pz3a2npyo4rpgzq use postgres instead'],
82
+ },
83
+ {
84
+ path: ['task', 'clear-done'],
85
+ summary: 'Clear every finished task from the board (0.25.0+)',
86
+ positionals: [],
87
+ options: PROJECT_FILTER,
88
+ description: 'Clears done and unverified tasks on every device. shard task show <id> still reads their reports.',
89
+ },
90
+ {
91
+ path: ['needs-you'],
92
+ summary: 'List what waits on you, oldest first (0.25.0+)',
93
+ positionals: [],
94
+ options: PROJECT_FILTER,
95
+ description: [
96
+ 'Everything your agents wait on, across the board: permission requests (perm_…), questions (cq_…, ask_…),',
97
+ 'credential requests (cred_…), finished work to land (land_…) and Later tasks to keep or clear (prune_…). Each line is',
98
+ 'the id, what is asked, and the command, options or branch behind it. Answer with shard needs-you answer <id>.',
99
+ '--json returns {data}.',
100
+ ].join('\n'),
101
+ examples: ['shard needs-you', 'shard needs-you --json'],
102
+ },
103
+ {
104
+ path: ['needs-you', 'answer'],
105
+ summary: 'Answer something that waits on you (0.25.0+)',
106
+ positionals: [{ name: 'id', required: true }, { name: 'decision' }],
107
+ options: {
108
+ everywhere: { type: 'boolean', description: 'With always: the rule covers every project.' },
109
+ text: { type: 'string', value: '<answer>', description: 'Your answer to an agent’s question.' },
110
+ pick: { type: 'string', multiple: true, value: '<option|question=option>', description: 'The option you pick for an agent’s question in a permission request (repeatable for a multi-select question; "<question>=<option>" when it asks several).' },
111
+ keep: { type: 'string', multiple: true, value: '<task-id>', description: 'Later tasks: keep these (repeatable); the rest are cleared.' },
112
+ },
113
+ description: [
114
+ 'One command for every kind:',
115
+ ' perm_… allow (this once), always (also adds the always-allow rule for the project; --everywhere for every',
116
+ ' project) or deny; an agent’s question takes --pick',
117
+ ' cq_… --text <answer>: the agent gets it as its next message',
118
+ ' ask_… --text <answer>: the task’s master gets it',
119
+ ' cred_… deny (the value goes through the credential request, sealed for its computer)',
120
+ ' land_… land (the worker merges its branch and pushes it), keep (the branch stays) or discard',
121
+ ' prune_… --keep <task-id> for each task to keep, keep for all of them, or discard to clear them',
122
+ 'The agent goes on as soon as you answer.',
123
+ ].join('\n'),
124
+ examples: ['shard needs-you answer perm_01a1300c-0000-7000-8000-000000000001 always', 'shard needs-you answer ask_rep_66cbevz65m72ij7mbiyrw4xvdy --text "eu-north-1"', 'shard needs-you answer land_conv_id4zts6jxsqraju2pggr55n3tq land'],
125
+ },
126
+ {
127
+ path: ['approvals'],
128
+ summary: 'Show the approval mode and the always-allow rules your agents work under (0.25.0+)',
129
+ positionals: [],
130
+ options: {},
131
+ description: `The mode answers the permission requests of your board’s conversations before they reach you: ${MODES} A rule always allows a tool for commands (or paths, URLs) that start with its pattern. Prints the default mode, each project’s own mode, then each rule: its id, tool, pattern and project.`,
132
+ },
133
+ {
134
+ path: ['approvals', 'set'],
135
+ summary: 'Set the approval mode of a project or the default (0.25.0+)',
136
+ positionals: [{ name: 'mode', required: true }],
137
+ options: { ...PROJECT, default: { type: 'boolean', description: 'Set the default mode, for every project without a mode of its own.' } },
138
+ description: `${MODES} default puts the project back on the default mode. Requests already waiting that the new mode covers are answered at once.`,
139
+ examples: ['shard approvals set trusted', 'shard approvals set ask --project 01a1415f-759f-7fb0-9d56-973ed5d27465', 'shard approvals set default'],
140
+ },
141
+ {
142
+ path: ['approvals', 'allow'],
143
+ summary: 'Always allow a tool for commands that start with a pattern (0.25.0+)',
144
+ positionals: [{ name: 'tool', required: true }, { name: 'pattern', variadic: true, words: true, required: true }],
145
+ options: { ...PROJECT, everywhere: { type: 'boolean', description: 'Cover every project.' } },
146
+ description: 'The tool as the agent’s harness names it (Bash, WebFetch, Edit) and the command prefix, path or URL it allows: "shard approvals allow Bash pnpm test" allows pnpm test and pnpm test --watch; "" allows every use of the tool. Waiting requests the rule covers are answered at once. Prints the rule’s id, tool, pattern and project.',
147
+ examples: ['shard approvals allow Bash pnpm test', 'shard approvals allow WebFetch https://docs.shardflux.dev/'],
148
+ },
149
+ {
150
+ path: ['approvals', 'remove'],
151
+ summary: 'Remove an always-allow rule (0.25.0+)',
152
+ positionals: [{ name: 'rule-id', required: true }],
153
+ options: {},
154
+ },
155
+ ];
@@ -0,0 +1,62 @@
1
+ /**
2
+ * The Orchestrator board (0.25.0+, SDK 0.28.0): `shard board` (with --follow), `shard task add|show|edit|rm|steer|
3
+ * clear-done`, `shard needs-you [answer]` and `shard approvals [set|allow|remove]`. The command table is board-args.ts.
4
+ *
5
+ * Every command uses the project API key (SHARDFLUX_API_KEY, else the saved one): its board is the key's project.
6
+ * Text output is one line per item, tab-separated, id first; --json prints the API's objects.
7
+ */
8
+ import type { ApprovalPolicy, ApprovalRule, Board, BoardUpdate, NeedsYouAnswer, NeedsYouAnswered, NeedsYouItem, Task, TaskDetail, TaskReport } from '@shardflux/sdk';
9
+ import type { Handler } from './commands.js';
10
+ /** A task as people read it: its title once it has one, else your words, on one line of at most `max` characters. */
11
+ export declare function taskLabel(t: Pick<Task, 'title' | 'text'>, max?: number): string;
12
+ /**
13
+ * One task as `shard board` lists it: id, state (with why it waits to start: "queued (Claude usage resets 15:00)"),
14
+ * title or words (a follow-up ↳, a suggestion marked), project.
15
+ */
16
+ export declare function taskLine(t: Task): string;
17
+ /** One item that waits on you: its id, what is asked, then the command, the suggested answers or the branch behind it. */
18
+ export declare function needsLine(n: NeedsYouItem): string;
19
+ /** A sealed report as --follow prints it: id, milestone, title, then its evidence labels. */
20
+ export declare function reportLine(r: TaskReport): string;
21
+ /** The board as text: Needs you first (oldest first), then Now, Next and Later in their order. */
22
+ export declare function boardText(b: Pick<Board, 'tasks' | 'needs_you'>): string;
23
+ /** The approval policy on one line: the default, each project's own mode, the number of rules. */
24
+ export declare function policyLine(p: ApprovalPolicy): string;
25
+ /** One always-allow rule: id, tool, the pattern it allows, its project. */
26
+ export declare function ruleLine(r: ApprovalRule): string;
27
+ /** `shard approvals`: the default mode, each project's own mode, then each rule. */
28
+ export declare function policyText(p: ApprovalPolicy): string;
29
+ /**
30
+ * `shard board --follow`: the lines of each feed answer. A changed task is its board line plus its bucket (deleted,
31
+ * cleared once gone); a sealed report its report line; an item that started waiting on you its needs line, one that
32
+ * stopped `<id>\tresolved`; a policy change one line. After a reset the whole board is printed again.
33
+ */
34
+ export declare class BoardFollower {
35
+ #private;
36
+ constructor(board: Pick<Board, 'needs_you'>);
37
+ next(u: BoardUpdate): string;
38
+ }
39
+ /** A feed answer as `board --follow --json` prints it: the API's fields (with `board` after a reset). */
40
+ export declare function feedJson(u: BoardUpdate): Record<string, unknown>;
41
+ /** `shard task show`: the task, its plan and conversations, then every report oldest first with its evidence. */
42
+ export declare function taskDetailText(d: TaskDetail): string;
43
+ /** An item's kind from its id (perm_, cq_, ask_, cred_, land_, prune_); undefined for another id. */
44
+ export declare function itemKind(id: string): NeedsYouItem['kind'] | undefined;
45
+ export interface AnswerInput {
46
+ word?: string | undefined;
47
+ text?: string | undefined;
48
+ picks: readonly string[];
49
+ keep: readonly string[];
50
+ everywhere: boolean;
51
+ }
52
+ /** Whether the answer needs the item itself: the questions of a permission request, every task of a prune prompt. */
53
+ export declare const answerNeedsItem: (id: string, a: AnswerInput) => boolean;
54
+ /**
55
+ * The API answer for item `id` from the command's words and options, checked before anything is sent: each kind takes
56
+ * its own (needs-you.ts). `item` is the waiting item when answerNeedsItem says it is needed; without it such an answer
57
+ * is only checked (its `answers` or `keep` come back empty).
58
+ */
59
+ export declare function needsAnswer(id: string, a: AnswerInput, item?: NeedsYouItem): NeedsYouAnswer;
60
+ /** What an answer did, in one line. */
61
+ export declare function answeredText(r: NeedsYouAnswered, a: NeedsYouAnswer): string;
62
+ export declare const BOARD_HANDLERS: Record<string, Handler>;
@@ -0,0 +1,395 @@
1
+ import { out, principal, str, strs } from "./commands.js";
2
+ import { detailLine, pickAnswers } from "./conversation-commands.js";
3
+ import { NotFoundError, UsageError } from "./errors.js";
4
+ import { time } from "./format.js";
5
+ import { progress } from "./progress.js";
6
+ const BUCKETS = ['now', 'next', 'later'];
7
+ const BUCKET_TITLES = { now: 'Now', next: 'Next', later: 'Later' };
8
+ const MODES = ['ask', 'routine', 'trusted'];
9
+ /** The board's client: the project API key's. */
10
+ const boardClient = (ctx) => ctx.cloud();
11
+ const oneLine = (s, max) => {
12
+ const line = s.replace(/\s+/g, ' ').trim();
13
+ return line.length > max ? `${line.slice(0, max - 1).trimEnd()}…` : line;
14
+ };
15
+ /** A task as people read it: its title once it has one, else your words, on one line of at most `max` characters. */
16
+ export function taskLabel(t, max = 80) {
17
+ return oneLine(t.title?.trim() || t.text, max);
18
+ }
19
+ /**
20
+ * One task as `shard board` lists it: id, state (with why it waits to start: "queued (Claude usage resets 15:00)"),
21
+ * title or words (a follow-up ↳, a suggestion marked), project.
22
+ */
23
+ export function taskLine(t) {
24
+ const state = `${t.state}${t.waiting?.message.trim() ? ` (${oneLine(t.waiting.message, 60)})` : ''}`;
25
+ const label = `${t.parent_task_id ? '↳ ' : ''}${taskLabel(t)}${t.suggested ? ' (suggested)' : ''}`;
26
+ return `${t.id}\t${state}\t${label}\t${t.project_id}${t.home ? ' (home)' : ''}`;
27
+ }
28
+ /** One item that waits on you: its id, what is asked, then the command, the suggested answers or the branch behind it. */
29
+ export function needsLine(n) {
30
+ const detail = n.permission ? detailLine(n.permission.detail) : n.options?.length ? n.options.join(' / ') : n.land ? n.land.branch : '';
31
+ return `${n.id}\t${oneLine(n.ask, 100)}${detail ? `\t${detail}` : ''}`;
32
+ }
33
+ /** A sealed report as --follow prints it: id, milestone, title, then its evidence labels. */
34
+ export function reportLine(r) {
35
+ const labels = r.evidence.map((e) => e.label).join(', ');
36
+ return `${r.id}\t${r.milestone}\t${oneLine(r.title, 100)}${labels ? `\t${labels}` : ''}`;
37
+ }
38
+ /** The board as text: Needs you first (oldest first), then Now, Next and Later in their order. */
39
+ export function boardText(b) {
40
+ const parts = [];
41
+ if (b.needs_you.length)
42
+ parts.push(['Needs you', ...b.needs_you.map(needsLine)].join('\n'));
43
+ for (const bucket of BUCKETS) {
44
+ const tasks = b.tasks.filter((t) => t.bucket === bucket && !t.deleted_at && !t.cleared_at).sort((x, y) => x.position - y.position);
45
+ if (tasks.length)
46
+ parts.push([BUCKET_TITLES[bucket], ...tasks.map(taskLine)].join('\n'));
47
+ }
48
+ return parts.length ? `${parts.join('\n\n')}\n` : 'Nothing on the board yet: shard task add <words> adds a task.\n';
49
+ }
50
+ /** The approval policy on one line: the default, each project's own mode, the number of rules. */
51
+ export function policyLine(p) {
52
+ const projects = Object.entries(p.projects).map(([id, mode]) => `; ${id}: ${mode}`).join('');
53
+ return `${p.default}${projects}${p.rules.length ? `; ${p.rules.length} rule${p.rules.length === 1 ? '' : 's'}` : ''}`;
54
+ }
55
+ /** One always-allow rule: id, tool, the pattern it allows, its project. */
56
+ export function ruleLine(r) {
57
+ return `${r.id}\t${r.tool}\t${r.pattern || '(every use)'}\t${r.project_id ?? 'every project'}`;
58
+ }
59
+ /** `shard approvals`: the default mode, each project's own mode, then each rule. */
60
+ export function policyText(p) {
61
+ return [`Default: ${p.default}`, ...Object.entries(p.projects).map(([id, mode]) => `Project ${id}: ${mode}`), ...p.rules.map(ruleLine)].join('\n') + '\n';
62
+ }
63
+ /**
64
+ * `shard board --follow`: the lines of each feed answer. A changed task is its board line plus its bucket (deleted,
65
+ * cleared once gone); a sealed report its report line; an item that started waiting on you its needs line, one that
66
+ * stopped `<id>\tresolved`; a policy change one line. After a reset the whole board is printed again.
67
+ */
68
+ export class BoardFollower {
69
+ #waiting;
70
+ constructor(board) {
71
+ this.#waiting = new Set(board.needs_you.map((n) => n.id));
72
+ }
73
+ next(u) {
74
+ if (u.reset) {
75
+ this.#waiting = new Set(u.board.needs_you.map((n) => n.id));
76
+ return `\n${boardText(u.board)}`;
77
+ }
78
+ const lines = [];
79
+ if (u.needs_you) {
80
+ const now = new Set(u.needs_you.map((n) => n.id));
81
+ for (const n of u.needs_you)
82
+ if (!this.#waiting.has(n.id))
83
+ lines.push(needsLine(n));
84
+ for (const id of this.#waiting)
85
+ if (!now.has(id))
86
+ lines.push(`${id}\tresolved`);
87
+ this.#waiting = now;
88
+ }
89
+ for (const t of u.tasks)
90
+ lines.push(`${taskLine(t)}\t${t.deleted_at ? 'deleted' : t.cleared_at ? 'cleared' : t.bucket}`);
91
+ for (const r of u.reports)
92
+ lines.push(reportLine(r));
93
+ if (u.policy)
94
+ lines.push(`Approvals: ${policyLine(u.policy)}`);
95
+ return lines.length ? `${lines.join('\n')}\n` : '';
96
+ }
97
+ }
98
+ /** A feed answer as `board --follow --json` prints it: the API's fields (with `board` after a reset). */
99
+ export function feedJson(u) {
100
+ const page = { ...u };
101
+ delete page.nextCursor;
102
+ delete page.held;
103
+ return page;
104
+ }
105
+ const evidenceText = (e) => {
106
+ const behind = e.detail ?? e.path;
107
+ return behind ? `${e.label} (${detailLine(behind, 60)})` : e.label;
108
+ };
109
+ /** `shard task show`: the task, its plan and conversations, then every report oldest first with its evidence. */
110
+ export function taskDetailText(d) {
111
+ const t = d.task;
112
+ const titled = Boolean(t.title?.trim());
113
+ const lines = [titled ? oneLine(t.title, 200) : oneLine(t.text.split('\n').find((l) => l.trim()) ?? t.text, 200)];
114
+ lines.push([`${t.state}${t.waiting?.message.trim() ? ` (${oneLine(t.waiting.message, 120)})` : ''}`, t.bucket, `${t.project_id}${t.home ? ' (home)' : ''}`, ...(t.suggested ? ['suggested'] : []), ...(t.deleted_at ? ['deleted'] : t.cleared_at ? ['cleared'] : [])].join('\t'));
115
+ // Your words exactly, when the first line does not already show them whole.
116
+ if (titled || t.text.trim() !== lines[0])
117
+ lines.push('', t.text);
118
+ if (t.plan) {
119
+ lines.push('', `Plan: ${t.plan.summary}`);
120
+ for (const s of t.plan.steps)
121
+ lines.push(` ${s.id}\t${s.state}\t${oneLine(s.text, 100)}${s.evidence.length ? `\t${s.evidence.join(', ')}` : ''}`);
122
+ }
123
+ if (t.conversations.length)
124
+ lines.push('', `Conversations: ${t.conversations.map((c) => `${c.id} (${c.role}${c.step_id ? ` ${c.step_id}` : ''})`).join(', ')}`);
125
+ if (d.reports.length) {
126
+ lines.push('', 'Reports');
127
+ for (const r of d.reports) {
128
+ lines.push(`${r.id}\t${r.milestone}\t${time(r.sealed_at)}\t${oneLine(r.title, 120)}`);
129
+ if (r.outcome.trim())
130
+ lines.push(` ${oneLine(r.outcome, 200)}`);
131
+ if (r.options?.length)
132
+ lines.push(` Options: ${r.options.join(' / ')}`);
133
+ if (r.evidence.length)
134
+ lines.push(` Evidence: ${r.evidence.map(evidenceText).join('; ')}`);
135
+ if (r.media.length)
136
+ lines.push(` Media: ${r.media.map((m) => `${m.name} (${m.path})`).join(', ')}`);
137
+ if (r.landed)
138
+ lines.push(` Landed: ${r.landed.how} ${r.landed.ref}`);
139
+ }
140
+ }
141
+ return `${lines.join('\n')}\n`;
142
+ }
143
+ /** --now, --next or --later (one at most); undefined when none was given. */
144
+ function bucketOf(values) {
145
+ const chosen = BUCKETS.filter((b) => values[b] === true);
146
+ if (chosen.length > 1)
147
+ throw new UsageError('Choose one of --now, --next or --later.');
148
+ return chosen[0];
149
+ }
150
+ /** --after <task-id> or --first (null: first); undefined when neither was given. */
151
+ function orderOf(values) {
152
+ const after = str(values.after), first = values.first === true;
153
+ if (after !== undefined && first)
154
+ throw new UsageError('Choose --after <task-id> or --first.');
155
+ return first ? null : after;
156
+ }
157
+ // ---- needs you -------------------------------------------------------------------------------
158
+ const KINDS = { perm: 'permission', cq: 'question', ask: 'question', cred: 'credential', land: 'land', prune: 'prune' };
159
+ /** An item's kind from its id (perm_, cq_, ask_, cred_, land_, prune_); undefined for another id. */
160
+ export function itemKind(id) {
161
+ return KINDS[id.slice(0, Math.max(0, id.indexOf('_')))];
162
+ }
163
+ /** The answer words: allow, always and deny (or the API's names), land, keep and discard. */
164
+ const WORDS = { allow: 'allow_once', allow_once: 'allow_once', always: 'allow_always', allow_always: 'allow_always', deny: 'deny', land: 'land', keep: 'keep', discard: 'discard' };
165
+ /** Whether the answer needs the item itself: the questions of a permission request, every task of a prune prompt. */
166
+ export const answerNeedsItem = (id, a) => a.picks.length > 0 || (itemKind(id) === 'prune' && a.word?.toLowerCase() === 'keep');
167
+ /**
168
+ * The API answer for item `id` from the command's words and options, checked before anything is sent: each kind takes
169
+ * its own (needs-you.ts). `item` is the waiting item when answerNeedsItem says it is needed; without it such an answer
170
+ * is only checked (its `answers` or `keep` come back empty).
171
+ */
172
+ export function needsAnswer(id, a, item) {
173
+ const kind = item?.kind ?? itemKind(id);
174
+ const decision = a.word === undefined ? undefined : WORDS[a.word.toLowerCase().replace(/-/g, '_')];
175
+ const only = (allowed, message) => {
176
+ const given = ['text', 'picks', 'keep', 'everywhere'].filter((k) => (k === 'text' ? a.text !== undefined : k === 'everywhere' ? a.everywhere : a[k].length > 0));
177
+ if (given.some((k) => !allowed.includes(k)))
178
+ throw new UsageError(message);
179
+ };
180
+ switch (kind) {
181
+ case 'permission': {
182
+ const message = 'Choose allow, always or deny (an agent’s question takes --pick <option>).';
183
+ only(['picks', 'everywhere'], message);
184
+ if (a.picks.length) {
185
+ if (a.word !== undefined)
186
+ throw new UsageError('Answer the question with --pick, or decline it with deny, not both.');
187
+ if (a.everywhere)
188
+ throw new UsageError('--everywhere goes with always.');
189
+ if (!item)
190
+ return { answers: {} };
191
+ return { answers: pickAnswers({ questions: (item.permission?.questions ?? []) }, a.picks) };
192
+ }
193
+ if (decision !== 'allow_once' && decision !== 'allow_always' && decision !== 'deny')
194
+ throw new UsageError(message);
195
+ if (a.everywhere && decision !== 'allow_always')
196
+ throw new UsageError('--everywhere goes with always.');
197
+ return decision === 'allow_always' ? { decision, scope: a.everywhere ? 'everywhere' : 'project' } : { decision };
198
+ }
199
+ case 'question': {
200
+ const message = 'Answer the question with --text <answer>.';
201
+ only(['text'], message);
202
+ if (a.word !== undefined || !a.text?.trim())
203
+ throw new UsageError(message);
204
+ return { text: a.text };
205
+ }
206
+ case 'credential':
207
+ only([], 'Provide the value through the credential request, sealed for its computer; deny declines it here.');
208
+ if (decision !== 'deny')
209
+ throw new UsageError('Provide the value through the credential request, sealed for its computer; deny declines it here.');
210
+ return { decision };
211
+ case 'land':
212
+ only([], 'Choose land, keep or discard.');
213
+ if (decision !== 'land' && decision !== 'keep' && decision !== 'discard')
214
+ throw new UsageError('Choose land, keep or discard.');
215
+ return { decision };
216
+ case 'prune': {
217
+ const message = 'Choose --keep <task-id> for each task to keep, keep for all of them, or discard to clear them.';
218
+ only(['keep'], message);
219
+ if (a.keep.length) {
220
+ if (a.word !== undefined)
221
+ throw new UsageError(message);
222
+ return { keep: [...new Set(a.keep)] };
223
+ }
224
+ if (decision === 'discard')
225
+ return { keep: [] };
226
+ if (decision === 'keep')
227
+ return { keep: item?.prune ? [...item.prune.task_ids] : [] };
228
+ throw new UsageError(message);
229
+ }
230
+ default: {
231
+ // An id of a kind this shard does not know yet: what was given goes to the API, which says what it takes.
232
+ const answer = {};
233
+ if (decision)
234
+ answer.decision = decision;
235
+ if (a.text !== undefined)
236
+ answer.text = a.text;
237
+ if (a.keep.length)
238
+ answer.keep = [...a.keep];
239
+ if (a.everywhere)
240
+ answer.scope = 'everywhere';
241
+ return answer;
242
+ }
243
+ }
244
+ }
245
+ const ANSWERED = { allow_once: 'Allowed once', allow_always: 'Always allowed', deny: 'Denied', land: 'Landing', keep: 'Kept', discard: 'Discarded' };
246
+ /** What an answer did, in one line. */
247
+ export function answeredText(r, a) {
248
+ const item = r.item;
249
+ if (a.answers)
250
+ return `Answered: ${Object.values(a.answers).map((v) => (Array.isArray(v) ? v.join(', ') : v)).join('; ')}\n`;
251
+ if (item.kind === 'prune' && a.keep) {
252
+ const all = item.prune?.task_ids.length ?? a.keep.length;
253
+ return a.keep.length === all ? `Kept all ${all} Later tasks.\n` : `Kept ${a.keep.length} of ${all} Later tasks; the rest are cleared.\n`;
254
+ }
255
+ if (item.kind === 'question')
256
+ return `Answered: ${oneLine(item.ask, 100)}\n`;
257
+ const what = item.kind === 'land' ? item.land?.branch ?? oneLine(item.ask, 100) : oneLine(item.ask, 100).replace(/\?$/, '');
258
+ return `${ANSWERED[a.decision ?? ''] ?? 'Answered'}: ${what}${r.rule_id ? ` (rule ${r.rule_id})` : ''}\n`;
259
+ }
260
+ // ---- handlers --------------------------------------------------------------------------------
261
+ const projectScope = (values) => (str(values.project) !== undefined ? { projectId: str(values.project) } : {});
262
+ export const BOARD_HANDLERS = {
263
+ // The board, or with --follow the board and then each change until Ctrl-C (the SDK's watch over the held feed,
264
+ // from the board's own cursor: nothing between the read and the first wait is missed).
265
+ async board(ctx, values) {
266
+ const cloud = boardClient(ctx), scope = projectScope(values);
267
+ const board = await cloud.board.get({ ...scope, signal: ctx.signal });
268
+ if (values.follow !== true) {
269
+ out(ctx, boardText(board), board);
270
+ return 0;
271
+ }
272
+ ctx.io.stdout.write(ctx.json ? `${JSON.stringify(board)}\n` : boardText(board));
273
+ progress(ctx, 'Following the board. Ctrl-C stops.');
274
+ const follower = new BoardFollower(board);
275
+ for await (const u of cloud.board.watch({ ...scope, cursor: board.cursor, signal: ctx.signal })) {
276
+ ctx.io.stdout.write(ctx.json ? `${JSON.stringify(feedJson(u))}\n` : follower.next(u));
277
+ }
278
+ return 0;
279
+ },
280
+ async 'task add'(ctx, values, words) {
281
+ const text = words.join(' ');
282
+ if (!text.trim())
283
+ throw new UsageError('missing <words...> for "shard task add"');
284
+ if (values.next === true && values.later === true)
285
+ throw new UsageError('Choose --next or --later.');
286
+ const bucket = values.later === true ? 'later' : values.next === true ? 'next' : undefined;
287
+ const after = orderOf(values);
288
+ const task = await boardClient(ctx).tasks.create({ text, ...projectScope(values), ...(values.home === true ? { home: true } : {}), ...(bucket ? { bucket } : {}), ...(after !== undefined ? { after } : {}) }, { signal: ctx.signal });
289
+ out(ctx, `${task.id}\t${task.bucket}\t${task.state}\n`, { task });
290
+ return 0;
291
+ },
292
+ async 'task show'(ctx, _values, [id]) {
293
+ const detail = await boardClient(ctx).tasks.get(id, { signal: ctx.signal });
294
+ out(ctx, taskDetailText(detail), detail);
295
+ return 0;
296
+ },
297
+ async 'task edit'(ctx, values, [id]) {
298
+ const p = {};
299
+ const text = str(values.text), title = str(values.title);
300
+ if (text !== undefined) {
301
+ if (!text.trim())
302
+ throw new UsageError('--text needs words.');
303
+ p.text = text;
304
+ }
305
+ if (title !== undefined)
306
+ p.title = title.trim() ? title : null;
307
+ const bucket = bucketOf(values), after = orderOf(values);
308
+ if (bucket)
309
+ p.bucket = bucket;
310
+ if (after !== undefined)
311
+ p.after = after;
312
+ if (values.keep === true)
313
+ p.suggested = false;
314
+ if (typeof values.clear === 'boolean')
315
+ p.cleared = values.clear;
316
+ if (Object.keys(p).length === 0)
317
+ throw new UsageError('Nothing to change: give --text, --title, --now, --next, --later, --after, --first, --keep or --clear.');
318
+ const task = await boardClient(ctx).tasks.update(id, p, { signal: ctx.signal });
319
+ out(ctx, `${taskLine(task)}\n`, { task });
320
+ return 0;
321
+ },
322
+ async 'task rm'(ctx, _values, [id]) {
323
+ const task = await boardClient(ctx).tasks.delete(id, { signal: ctx.signal });
324
+ out(ctx, `Deleted: ${taskLabel(task)}\n`, { task });
325
+ return 0;
326
+ },
327
+ async 'task steer'(ctx, _values, [id, ...words]) {
328
+ const text = words.join(' ');
329
+ if (!text.trim())
330
+ throw new UsageError('missing <words...> for "shard task steer"');
331
+ const task = await boardClient(ctx).tasks.steer(id, text, { signal: ctx.signal });
332
+ out(ctx, `Steered: ${taskLabel(task)}\n`, { task });
333
+ return 0;
334
+ },
335
+ async 'task clear-done'(ctx, values) {
336
+ const cleared = await boardClient(ctx).tasks.clearDone(projectScope(values), { signal: ctx.signal });
337
+ out(ctx, cleared.length ? `Cleared ${cleared.length} finished task${cleared.length === 1 ? '' : 's'}.\n` : 'No finished tasks to clear.\n', { cleared });
338
+ return 0;
339
+ },
340
+ async 'needs-you'(ctx, values) {
341
+ const data = await boardClient(ctx).needsYou.list({ ...projectScope(values), signal: ctx.signal });
342
+ out(ctx, data.length ? `${data.map(needsLine).join('\n')}\n` : 'Nothing waits on you.\n', { data });
343
+ return 0;
344
+ },
345
+ async 'needs-you answer'(ctx, values, [id, word]) {
346
+ const input = { word, text: str(values.text), picks: strs(values.pick), keep: strs(values.keep), everywhere: values.everywhere === true };
347
+ const cloud = boardClient(ctx);
348
+ // Checked first, so a wrong combination is a usage error before any request.
349
+ let answer = needsAnswer(id, input);
350
+ if (answerNeedsItem(id, input)) {
351
+ const item = (await cloud.needsYou.list({ signal: ctx.signal })).find((n) => n.id === id);
352
+ if (!item)
353
+ throw new NotFoundError('Nothing waits on you there any more.', 'shard needs-you lists what does', { reason: 'needs_you_not_found' });
354
+ answer = needsAnswer(id, input, item);
355
+ }
356
+ const r = await cloud.needsYou.answer(id, answer, { signal: ctx.signal });
357
+ out(ctx, answeredText(r, answer), r);
358
+ return 0;
359
+ },
360
+ async approvals(ctx) {
361
+ const policy = await boardClient(ctx).approvals.getPolicy({ signal: ctx.signal });
362
+ out(ctx, policyText(policy), policy);
363
+ return 0;
364
+ },
365
+ async 'approvals set'(ctx, values, [word]) {
366
+ const w = word.toLowerCase();
367
+ if (w !== 'default' && !MODES.includes(w))
368
+ throw new UsageError('Choose ask, routine or trusted (default puts the project back on the default mode).');
369
+ const mode = w === 'default' ? null : w, project = str(values.project);
370
+ if (values.default === true && project !== undefined)
371
+ throw new UsageError('Choose --project <id> or --default.');
372
+ if (values.default === true && mode === null)
373
+ throw new UsageError('--default takes ask, routine or trusted.');
374
+ const cloud = boardClient(ctx);
375
+ const params = values.default === true ? { default: mode } : { projects: { [project ?? (await principal(ctx)).project_id]: mode } };
376
+ const policy = await cloud.approvals.setPolicy(params, { signal: ctx.signal });
377
+ out(ctx, policyText(policy), policy);
378
+ return 0;
379
+ },
380
+ async 'approvals allow'(ctx, values, [tool, ...pattern]) {
381
+ const project = str(values.project), everywhere = values.everywhere === true;
382
+ if (project !== undefined && everywhere)
383
+ throw new UsageError('Choose --project <id> or --everywhere.');
384
+ if (!tool.trim())
385
+ throw new UsageError('Name the tool, such as Bash, WebFetch or Edit.');
386
+ const rule = await boardClient(ctx).approvals.addRule({ tool: tool, pattern: pattern.join(' '), ...(everywhere ? { projectId: null } : project !== undefined ? { projectId: project } : {}) }, { signal: ctx.signal });
387
+ out(ctx, `${ruleLine(rule)}\n`, rule);
388
+ return 0;
389
+ },
390
+ async 'approvals remove'(ctx, _values, [id]) {
391
+ const policy = await boardClient(ctx).approvals.removeRule(id, { signal: ctx.signal });
392
+ out(ctx, `Removed ${id}.\n`, policy);
393
+ return 0;
394
+ },
395
+ };