toolroll 0.8.0 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/README.md +5 -5
- package/dist/agent-onboard.d.ts +41 -18
- package/dist/agent-onboard.js +129 -55
- package/dist/browser/workspace.css +1 -1
- package/dist/browser/workspace.js +14 -13
- package/dist/daemon.d.ts +6 -2
- package/dist/daemon.js +6 -5
- package/dist/first-run.d.ts +4 -1
- package/dist/first-run.js +10 -7
- package/dist/install-method.d.ts +1 -1
- package/dist/install-method.js +4 -0
- package/dist/operate.d.ts +2 -0
- package/dist/operate.js +64 -9
- package/dist/prompt.d.ts +6 -0
- package/dist/prompt.js +12 -0
- package/dist/serve.js +49 -23
- package/dist/store.d.ts +1 -1
- package/dist/store.js +5 -4
- package/dist/toolroll-update-cli.js +7 -3
- package/dist/toolroll-update-ui.js +6 -5
- package/dist/toolroll-update.d.ts +46 -12
- package/dist/toolroll-update.js +423 -99
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.8.1 — 2026-09-30
|
|
6
|
+
|
|
7
|
+
- **Safer `toolroll update`.** A release is accepted only when its signing
|
|
8
|
+
certificate names ap9000/toolroll, its publish workflow and GitHub
|
|
9
|
+
Actions, checked with the Sigstore verifier npm already ships (found from
|
|
10
|
+
npm itself, so a custom npm global folder works too). A failed
|
|
11
|
+
update puts the commands back before stopping the service, stops and
|
|
12
|
+
restarts per-project watch workers with it, restores even when the live
|
|
13
|
+
database can't be copied aside, and keeps the last good update so
|
|
14
|
+
`--rollback` still works after a refused attempt, including for the update
|
|
15
|
+
that brought 0.8.1. The console's update job
|
|
16
|
+
finds pnpm, bun and npm where they are installed; the desktop app is never
|
|
17
|
+
offered npm.
|
|
18
|
+
- **Safer `toolroll onboard`.** It adds the main checkout rather than a
|
|
19
|
+
temporary worktree, never adds your home folder, and asks before adding a
|
|
20
|
+
project. Run by an agent, `toolroll up` never prints the password. Login
|
|
21
|
+
advice matches how the install actually signs in, and uses the port `up`
|
|
22
|
+
serves on. A skill you have edited is left alone.
|
|
23
|
+
- **Chat opens instantly.** First-run suggestions and the agent sign-in check
|
|
24
|
+
refresh in the background, so a slow network never holds up the page.
|
|
25
|
+
Tapping a suggestion adds to what you typed instead of replacing it.
|
|
26
|
+
|
|
5
27
|
## 0.8.0 — 2026-09-30
|
|
6
28
|
|
|
7
29
|
- **Install with your agent.** `toolroll onboard`, run by the agent that
|
package/README.md
CHANGED
|
@@ -53,13 +53,13 @@ Paste this into Claude Code or Codex:
|
|
|
53
53
|
|
|
54
54
|
> Install Toolroll with `npm install -g toolroll` and start `toolroll up` in
|
|
55
55
|
> the background; it keeps running. Then run `toolroll onboard` in this
|
|
56
|
-
> repository, and run it again with `--yes` to
|
|
57
|
-
> you. Tell me the console address, where my login is saved (not the
|
|
56
|
+
> repository, and run it again with `--yes` to add it as a project and install
|
|
57
|
+
> Toolroll's skill for you. Tell me the console address, where my login is saved (not the
|
|
58
58
|
> password), how to pair my phone, and what I can ask you next.
|
|
59
59
|
|
|
60
|
-
`toolroll onboard` adds the repository as a project,
|
|
61
|
-
signed in, and prints the line that adds Toolroll as
|
|
62
|
-
(`claude mcp add toolroll -- toolroll mcp`) without running it.
|
|
60
|
+
`toolroll onboard --yes` adds the repository (its main checkout) as a project,
|
|
61
|
+
says which agent CLIs are signed in, and prints the line that adds Toolroll as
|
|
62
|
+
tools (`claude mcp add --scope user toolroll -- toolroll mcp`) without running it.
|
|
63
63
|
`toolroll onboard --remove --yes` takes the skill out again.
|
|
64
64
|
|
|
65
65
|
Then read [Getting started](docs/guide/getting-started.md), [Flows](docs/guide/flows.md)
|
package/dist/agent-onboard.d.ts
CHANGED
|
@@ -7,29 +7,34 @@
|
|
|
7
7
|
* a thin operator skill for the person's own agent, prints the MCP line
|
|
8
8
|
* without running it, and ends with a handoff the agent can relay.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* Adding the project and writing the skill both need --yes or one yes
|
|
11
|
+
* typed at a terminal. The project is the main checkout (never a linked
|
|
12
|
+
* worktree, the home folder, or one of Toolroll's own worktrees). The skill
|
|
13
|
+
* is replaced by the next onboard and deleted by `onboard --remove` only
|
|
14
|
+
* while it is exactly a version Toolroll wrote; a file of the same name
|
|
15
|
+
* that is not ours, or ours with the person's edits, is never touched.
|
|
14
16
|
*/
|
|
15
17
|
import type { ProviderConnection } from "./provider-connection.js";
|
|
16
18
|
export type OnboardAgent = "claude" | "codex";
|
|
17
19
|
export declare const ONBOARD_AGENTS: readonly OnboardAgent[];
|
|
18
20
|
export declare const AGENT_NAMES: Record<OnboardAgent, string>;
|
|
19
|
-
/** Each agent's user-level home: where its skills folder lives.
|
|
21
|
+
/** Each agent's user-level home: where its skills folder lives. A relative CODEX_HOME is under the home folder, never
|
|
22
|
+
* wherever onboard happens to run. */
|
|
20
23
|
export declare function agentHome(agent: OnboardAgent, home: string, env: Record<string, string | undefined>): string;
|
|
21
24
|
export declare function operatorSkillPath(agent: OnboardAgent, home: string, env: Record<string, string | undefined>): string;
|
|
22
25
|
/** The agents this person uses: those whose home folder exists, else Claude Code. */
|
|
23
26
|
export declare function detectAgents(home: string, env: Record<string, string | undefined>): OnboardAgent[];
|
|
24
|
-
/** The line that adds Toolroll as tools. Printed, never run. */
|
|
27
|
+
/** The line that adds Toolroll as tools, for every project. Printed, never run. */
|
|
25
28
|
export declare function mcpLine(agent: OnboardAgent): string;
|
|
26
29
|
/** The thin skill: when to reach for Toolroll, and where the real guides are. */
|
|
27
30
|
export declare function operatorSkillContent(version: string): string;
|
|
31
|
+
/** A skill's fingerprint with the version it names set aside: every release writes the same words. */
|
|
32
|
+
export declare function skillFingerprint(content: string): string;
|
|
28
33
|
export type SkillStep = {
|
|
29
34
|
agent: OnboardAgent;
|
|
30
35
|
path: string;
|
|
31
|
-
/** create/replace/remove change the file; current and absent change nothing; not-ours
|
|
32
|
-
action: "create" | "replace" | "current" | "remove" | "absent" | "not-ours";
|
|
36
|
+
/** create/replace/remove change the file; current and absent change nothing; not-ours and edited refuse. */
|
|
37
|
+
action: "create" | "replace" | "current" | "remove" | "absent" | "not-ours" | "edited";
|
|
33
38
|
/** Old guide copies of ours that go with it. */
|
|
34
39
|
alsoRemove: string[];
|
|
35
40
|
};
|
|
@@ -45,23 +50,24 @@ export declare function applyOperatorSkill(steps: readonly SkillStep[], version:
|
|
|
45
50
|
wrote: string[];
|
|
46
51
|
removed: string[];
|
|
47
52
|
};
|
|
53
|
+
/** How to sign in: the account, and the saved login file when one still works. Never the password. */
|
|
54
|
+
export type HandoffLogin = {
|
|
55
|
+
account: string | null;
|
|
56
|
+
file: string | null;
|
|
57
|
+
};
|
|
48
58
|
export type Handoff = {
|
|
49
59
|
console: string;
|
|
50
60
|
/** How to start the console when it is not running. */
|
|
51
61
|
start: string;
|
|
52
|
-
/**
|
|
53
|
-
login:
|
|
54
|
-
account: string | null;
|
|
55
|
-
file: string;
|
|
56
|
-
} | null;
|
|
62
|
+
/** null while there is no account yet. */
|
|
63
|
+
login: HandoffLogin | null;
|
|
57
64
|
phone: string;
|
|
58
65
|
next: readonly string[];
|
|
59
66
|
};
|
|
60
67
|
export declare const NEXT_THINGS: readonly ["queue these bugs overnight", "what needs me?", "open the result"];
|
|
61
68
|
export declare function buildHandoff(input: {
|
|
62
69
|
url: string;
|
|
63
|
-
|
|
64
|
-
account: string | null;
|
|
70
|
+
login: HandoffLogin | null;
|
|
65
71
|
}): Handoff;
|
|
66
72
|
export declare function handoffLines(handoff: Handoff): string[];
|
|
67
73
|
/** The account name from a saved login file, never its password. */
|
|
@@ -82,16 +88,24 @@ export type OnboardIo = {
|
|
|
82
88
|
/** --agent, as typed: a comma list of claude and codex. */
|
|
83
89
|
agentFlag: string | undefined;
|
|
84
90
|
url: string;
|
|
85
|
-
|
|
91
|
+
login: HandoffLogin | null;
|
|
86
92
|
home: string;
|
|
93
|
+
/** Where Toolroll leases its own worktrees: never a project. */
|
|
94
|
+
worktrees: string;
|
|
87
95
|
env: Record<string, string | undefined>;
|
|
88
96
|
version: string;
|
|
89
97
|
cwd: string;
|
|
90
98
|
/** A person at a terminal who can be asked y/N. */
|
|
91
99
|
interactive: boolean;
|
|
92
100
|
confirm: (question: string) => Promise<boolean>;
|
|
93
|
-
/** The repository top folder
|
|
94
|
-
|
|
101
|
+
/** The repository containing cwd: its top folder, and its main checkout (the same folder unless cwd is in a linked
|
|
102
|
+
* worktree). null outside one. */
|
|
103
|
+
findRepo: (cwd: string) => Promise<{
|
|
104
|
+
top: string;
|
|
105
|
+
main: string;
|
|
106
|
+
} | null>;
|
|
107
|
+
/** Whether a repository is already a project. */
|
|
108
|
+
enrolled: (repo: string) => Promise<boolean>;
|
|
95
109
|
/** Add a repository as a project, as Projects → add does. */
|
|
96
110
|
enroll: (repo: string) => Promise<{
|
|
97
111
|
ok: true;
|
|
@@ -102,5 +116,14 @@ export type OnboardIo = {
|
|
|
102
116
|
}>;
|
|
103
117
|
checkConnection: (agent: OnboardAgent) => Promise<ProviderConnection>;
|
|
104
118
|
};
|
|
119
|
+
/** Why a found repository cannot be the project, or null when it can. */
|
|
120
|
+
export declare function projectRefusal(found: {
|
|
121
|
+
top: string;
|
|
122
|
+
main: string;
|
|
123
|
+
}, where: {
|
|
124
|
+
cwd: string;
|
|
125
|
+
home: string;
|
|
126
|
+
worktrees: string;
|
|
127
|
+
}): string | null;
|
|
105
128
|
/** Returns the exit code: 0 done, 2 usage, 3 a refusal to answer. */
|
|
106
129
|
export declare function runOnboard(io: OnboardIo): Promise<number>;
|
package/dist/agent-onboard.js
CHANGED
|
@@ -7,22 +7,30 @@
|
|
|
7
7
|
* a thin operator skill for the person's own agent, prints the MCP line
|
|
8
8
|
* without running it, and ends with a handoff the agent can relay.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* Adding the project and writing the skill both need --yes or one yes
|
|
11
|
+
* typed at a terminal. The project is the main checkout (never a linked
|
|
12
|
+
* worktree, the home folder, or one of Toolroll's own worktrees). The skill
|
|
13
|
+
* is replaced by the next onboard and deleted by `onboard --remove` only
|
|
14
|
+
* while it is exactly a version Toolroll wrote; a file of the same name
|
|
15
|
+
* that is not ours, or ours with the person's edits, is never touched.
|
|
14
16
|
*/
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
17
|
+
import { createHash } from "node:crypto";
|
|
18
|
+
import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, realpathSync, rmdirSync, unlinkSync, writeFileSync } from "node:fs";
|
|
19
|
+
import { isAbsolute, join, resolve, sep } from "node:path";
|
|
20
|
+
import { CLAUDE_CODE_GUIDES, CLAUDE_CODE_MANAGED_MARK, MANAGED_MARK, OPERATOR_SKILL_MARK, claudeCodeSkillContent } from "./skills.js";
|
|
18
21
|
import { connectionWords } from "./control-ui.js";
|
|
19
22
|
import { envelopeJson } from "./envelope.js";
|
|
20
23
|
export const ONBOARD_AGENTS = ["claude", "codex"];
|
|
21
24
|
export const AGENT_NAMES = { claude: "Claude Code", codex: "Codex" };
|
|
22
|
-
/** Each agent's user-level home: where its skills folder lives.
|
|
25
|
+
/** Each agent's user-level home: where its skills folder lives. A relative CODEX_HOME is under the home folder, never
|
|
26
|
+
* wherever onboard happens to run. */
|
|
23
27
|
export function agentHome(agent, home, env) {
|
|
24
|
-
if (agent === "codex")
|
|
25
|
-
|
|
28
|
+
if (agent === "codex") {
|
|
29
|
+
const codexHome = env["CODEX_HOME"];
|
|
30
|
+
if (codexHome === undefined || codexHome === "")
|
|
31
|
+
return join(home, ".codex");
|
|
32
|
+
return isAbsolute(codexHome) ? codexHome : resolve(home, codexHome);
|
|
33
|
+
}
|
|
26
34
|
return join(home, ".claude");
|
|
27
35
|
}
|
|
28
36
|
export function operatorSkillPath(agent, home, env) {
|
|
@@ -33,9 +41,9 @@ export function detectAgents(home, env) {
|
|
|
33
41
|
const found = ONBOARD_AGENTS.filter(agent => existsSync(agentHome(agent, home, env)));
|
|
34
42
|
return found.length > 0 ? found : ["claude"];
|
|
35
43
|
}
|
|
36
|
-
/** The line that adds Toolroll as tools. Printed, never run. */
|
|
44
|
+
/** The line that adds Toolroll as tools, for every project. Printed, never run. */
|
|
37
45
|
export function mcpLine(agent) {
|
|
38
|
-
return `${agent} mcp add toolroll -- toolroll mcp`;
|
|
46
|
+
return agent === "claude" ? "claude mcp add --scope user toolroll -- toolroll mcp" : `${agent} mcp add toolroll -- toolroll mcp`;
|
|
39
47
|
}
|
|
40
48
|
/** The thin skill: when to reach for Toolroll, and where the real guides are. */
|
|
41
49
|
export function operatorSkillContent(version) {
|
|
@@ -64,13 +72,24 @@ Read the guides from the installed binary. They match its version, so do not cop
|
|
|
64
72
|
Approving work and answering decisions belong to the person. Never read out or repeat their password.
|
|
65
73
|
`;
|
|
66
74
|
}
|
|
67
|
-
|
|
68
|
-
|
|
75
|
+
const escapedMark = OPERATOR_SKILL_MARK.replace(/[.*+?^$()|[\]\\{}]/g, "\\$&");
|
|
76
|
+
const WRITTEN_BY = new RegExp(`^(${escapedMark} · written by toolroll )(\\S+?)(\\. \`toolroll onboard)`, "m");
|
|
77
|
+
/** A skill's fingerprint with the version it names set aside: every release writes the same words. */
|
|
78
|
+
export function skillFingerprint(content) {
|
|
79
|
+
return createHash("sha256").update(content.replace(WRITTEN_BY, "$1*$3")).digest("hex");
|
|
80
|
+
}
|
|
81
|
+
/** Every SKILL.md Toolroll has generated at this path: the operator skill, and `skills install --claude-code`'s. */
|
|
82
|
+
const KNOWN_SKILLS = new Set([operatorSkillContent("0.0.0"), claudeCodeSkillContent()].map(skillFingerprint));
|
|
83
|
+
/** ours: exactly a version Toolroll wrote. edited: ours, with the person's changes. foreign: not ours at all. */
|
|
84
|
+
function skillOwnership(path) {
|
|
69
85
|
const entry = lstatSync(path);
|
|
70
86
|
if (entry.isSymbolicLink() || !entry.isFile() || entry.size > 2_000_000)
|
|
71
|
-
return
|
|
87
|
+
return "foreign";
|
|
72
88
|
const content = readFileSync(path, "utf8");
|
|
73
|
-
|
|
89
|
+
const marked = content.startsWith("---\nname: toolroll\n") && [OPERATOR_SKILL_MARK, CLAUDE_CODE_MANAGED_MARK, MANAGED_MARK].some(mark => content.includes(mark));
|
|
90
|
+
if (!marked)
|
|
91
|
+
return "foreign";
|
|
92
|
+
return KNOWN_SKILLS.has(skillFingerprint(content)) ? "ours" : "edited";
|
|
74
93
|
}
|
|
75
94
|
/** Guide copies an older `skills install --claude-code` left beside SKILL.md. */
|
|
76
95
|
function oldGuideCopies(folder) {
|
|
@@ -103,8 +122,9 @@ export function planOperatorSkill(agents, options) {
|
|
|
103
122
|
if (!lstatExists(path)) {
|
|
104
123
|
return { agent, path, action: options.remove ? (alsoRemove.length > 0 ? "remove" : "absent") : "create", alsoRemove };
|
|
105
124
|
}
|
|
106
|
-
|
|
107
|
-
|
|
125
|
+
const ownership = skillOwnership(path);
|
|
126
|
+
if (ownership !== "ours")
|
|
127
|
+
return { agent, path, action: ownership === "edited" ? "edited" : "not-ours", alsoRemove: [] };
|
|
108
128
|
if (options.remove)
|
|
109
129
|
return { agent, path, action: "remove", alsoRemove };
|
|
110
130
|
const current = readFileSync(path, "utf8") === operatorSkillContent(options.version);
|
|
@@ -147,15 +167,17 @@ export function buildHandoff(input) {
|
|
|
147
167
|
return {
|
|
148
168
|
console: input.url,
|
|
149
169
|
start: "toolroll up",
|
|
150
|
-
login: input.
|
|
170
|
+
login: input.login,
|
|
151
171
|
phone: "Open the console on your phone over your tailnet, or pair Telegram in Settings → Telegram with the /pair code it shows.",
|
|
152
172
|
next: NEXT_THINGS,
|
|
153
173
|
};
|
|
154
174
|
}
|
|
155
175
|
export function handoffLines(handoff) {
|
|
156
176
|
const login = handoff.login === null
|
|
157
|
-
? "no
|
|
158
|
-
:
|
|
177
|
+
? "no account yet; `toolroll up` creates one"
|
|
178
|
+
: handoff.login.file === null
|
|
179
|
+
? `${handoff.login.account ?? "your account"} — sign in with your password`
|
|
180
|
+
: `${handoff.login.account ?? "the account"} — the password is in ${handoff.login.file}`;
|
|
159
181
|
return [
|
|
160
182
|
"Toolroll is ready.",
|
|
161
183
|
` console ${handoff.console}`,
|
|
@@ -178,6 +200,27 @@ export function loginAccount(file) {
|
|
|
178
200
|
export function agentReport(agent, connection) {
|
|
179
201
|
return { agent, name: AGENT_NAMES[agent], state: connection.state, words: connectionWords(connection), ...(connection.plan === undefined ? {} : { plan: connection.plan }) };
|
|
180
202
|
}
|
|
203
|
+
const real = (path) => {
|
|
204
|
+
try {
|
|
205
|
+
return realpathSync(path);
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
return resolve(path);
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
const within = (path, folder) => path === folder || path.startsWith(folder.endsWith(sep) ? folder : `${folder}${sep}`);
|
|
212
|
+
/** Why a found repository cannot be the project, or null when it can. */
|
|
213
|
+
export function projectRefusal(found, where) {
|
|
214
|
+
const worktrees = real(where.worktrees);
|
|
215
|
+
if ([where.cwd, found.top, found.main].some(path => within(real(path), worktrees))) {
|
|
216
|
+
return `${found.top} is one of Toolroll's own worktrees, not a project: run onboard inside your own checkout`;
|
|
217
|
+
}
|
|
218
|
+
const home = real(where.home);
|
|
219
|
+
if (real(found.main) === home || real(found.top) === home) {
|
|
220
|
+
return `${home} is your home folder, not a project: run onboard inside the repository you want to hand off`;
|
|
221
|
+
}
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
181
224
|
/** Returns the exit code: 0 done, 2 usage, 3 a refusal to answer. */
|
|
182
225
|
export async function runOnboard(io) {
|
|
183
226
|
const command = "onboard";
|
|
@@ -196,70 +239,101 @@ export async function runOnboard(io) {
|
|
|
196
239
|
else {
|
|
197
240
|
agents = detectAgents(io.home, io.env);
|
|
198
241
|
}
|
|
199
|
-
// The skill: validate every file first
|
|
242
|
+
// The skill: validate every file first.
|
|
200
243
|
const steps = planOperatorSkill(agents, { home: io.home, env: io.env, version: io.version, remove: io.remove });
|
|
201
244
|
const changes = steps.filter(step => step.action === "create" || step.action === "replace" || step.action === "remove");
|
|
202
|
-
const foreign = steps.filter(step => step.action === "not-ours");
|
|
245
|
+
const foreign = steps.filter(step => step.action === "not-ours" || step.action === "edited");
|
|
246
|
+
const skillChanges = foreign.length === 0 && changes.length > 0;
|
|
247
|
+
// The project: the main checkout of the repository onboard runs in, never --remove's business.
|
|
248
|
+
let project = { state: "none" };
|
|
249
|
+
if (!io.remove) {
|
|
250
|
+
const found = await io.findRepo(io.cwd);
|
|
251
|
+
if (found !== null) {
|
|
252
|
+
const refusal = projectRefusal(found, { cwd: io.cwd, home: io.home, worktrees: io.worktrees });
|
|
253
|
+
if (refusal !== null)
|
|
254
|
+
project = { state: "refused", path: found.main, message: refusal };
|
|
255
|
+
else
|
|
256
|
+
project = { state: (await io.enrolled(found.main)) ? "already" : "needs-yes", path: found.main };
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
const projectChange = project.state === "needs-yes";
|
|
260
|
+
// Then ask once for everything that writes, then write.
|
|
261
|
+
let consent = false;
|
|
262
|
+
if (skillChanges || projectChange) {
|
|
263
|
+
const parts = [
|
|
264
|
+
...(projectChange ? [`add ${project.path} as a project`] : []),
|
|
265
|
+
...(skillChanges ? [`${io.remove ? "remove" : "write"} ${changes.map(step => step.path).join(" and ")}`] : []),
|
|
266
|
+
];
|
|
267
|
+
const question = `${parts.join(" and ").replace(/^./, first => first.toUpperCase())}? [y/N]`;
|
|
268
|
+
consent = io.yes || (io.interactive && !io.json && (await io.confirm(question)));
|
|
269
|
+
}
|
|
270
|
+
const unconfirmed = io.interactive && !io.json && !io.yes ? "declined" : "needs-yes";
|
|
203
271
|
let skill;
|
|
204
272
|
if (foreign.length > 0) {
|
|
205
|
-
skill = { state: "not-ours", files: steps, wrote: [], removed: [] };
|
|
273
|
+
skill = { state: foreign.some(step => step.action === "not-ours") ? "not-ours" : "edited", files: steps, wrote: [], removed: [] };
|
|
206
274
|
}
|
|
207
|
-
else if (
|
|
275
|
+
else if (!skillChanges) {
|
|
208
276
|
skill = { state: io.remove ? "absent" : "current", files: steps, wrote: [], removed: [] };
|
|
209
277
|
}
|
|
278
|
+
else if (!consent) {
|
|
279
|
+
skill = { state: unconfirmed, files: steps, wrote: [], removed: [] };
|
|
280
|
+
}
|
|
210
281
|
else {
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
if (!consent) {
|
|
214
|
-
skill = { state: io.interactive && !io.json && !io.yes ? "declined" : "needs-yes", files: steps, wrote: [], removed: [] };
|
|
215
|
-
}
|
|
216
|
-
else {
|
|
217
|
-
const done = applyOperatorSkill(steps, io.version);
|
|
218
|
-
skill = { state: io.remove ? "removed" : "written", files: steps, ...done };
|
|
219
|
-
}
|
|
282
|
+
const done = applyOperatorSkill(steps, io.version);
|
|
283
|
+
skill = { state: io.remove ? "removed" : "written", files: steps, ...done };
|
|
220
284
|
}
|
|
221
285
|
const skillLine = () => {
|
|
222
286
|
const where = (list) => list.map(step => step.path).join(", ");
|
|
287
|
+
const nothing = io.remove ? "removed" : "written";
|
|
223
288
|
switch (skill.state) {
|
|
224
289
|
case "written": return `skill wrote ${skill.wrote.join(", ")}`;
|
|
225
290
|
case "removed": return `skill removed ${skill.removed.join(", ")}`;
|
|
226
291
|
case "current": return `skill up to date: ${where(steps)}`;
|
|
227
292
|
case "absent": return "skill not installed; nothing to remove";
|
|
228
|
-
case "not-ours": return `skill ${where(foreign)} ${foreign.length === 1 ? "is" : "are"} not Toolroll's, so nothing was ${
|
|
229
|
-
case "
|
|
230
|
-
case "
|
|
293
|
+
case "not-ours": return `skill ${where(foreign)} ${foreign.length === 1 ? "is" : "are"} not Toolroll's, so nothing was ${nothing}`;
|
|
294
|
+
case "edited": return `skill ${where(foreign)} ${foreign.length === 1 ? "has your edits, so it was" : "have your edits, so they were"} left as is`;
|
|
295
|
+
case "declined": return `skill nothing ${nothing}`;
|
|
296
|
+
case "needs-yes": return `skill not ${nothing} yet: run again with --yes to ${io.remove ? "remove" : "write"} ${where(changes)}`;
|
|
231
297
|
}
|
|
232
298
|
};
|
|
233
299
|
const skillData = { state: skill.state, files: skill.files.map(step => ({ agent: step.agent, path: step.path, action: step.action })), wrote: skill.wrote, removed: skill.removed };
|
|
234
300
|
if (io.remove) {
|
|
235
|
-
if (skill.state === "not-ours")
|
|
236
|
-
return refuse(
|
|
301
|
+
if (skill.state === "not-ours" || skill.state === "edited")
|
|
302
|
+
return refuse(skill.state, skillLine().replace(/^skill\s+/, ""), 3, { skill: skillData });
|
|
237
303
|
if (skill.state === "needs-yes" || skill.state === "declined") {
|
|
238
304
|
return refuse("unconfirmed", skillLine().replace(/^skill\s+/, ""), 3, { skill: skillData });
|
|
239
305
|
}
|
|
240
306
|
io.write(io.json ? envelopeJson({ ok: true, command, skill: skillData }) : skillLine());
|
|
241
307
|
return 0;
|
|
242
308
|
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
project = { path: repo, added: enrolled.added };
|
|
251
|
-
else
|
|
252
|
-
projectProblem = enrolled.message;
|
|
309
|
+
if (project.state === "needs-yes") {
|
|
310
|
+
if (!consent)
|
|
311
|
+
project = { state: unconfirmed, path: project.path };
|
|
312
|
+
else {
|
|
313
|
+
const enrolled = await io.enroll(project.path);
|
|
314
|
+
project = enrolled.ok ? { state: enrolled.added ? "added" : "already", path: project.path } : { state: "failed", path: project.path, message: enrolled.message };
|
|
315
|
+
}
|
|
253
316
|
}
|
|
254
317
|
const reports = await Promise.all(ONBOARD_AGENTS.map(async (agent) => agentReport(agent, await io.checkConnection(agent))));
|
|
255
|
-
const handoff = buildHandoff({ url: io.url,
|
|
318
|
+
const handoff = buildHandoff({ url: io.url, login: io.login });
|
|
256
319
|
const mcp = agents.map(agent => ({ agent, command: mcpLine(agent) }));
|
|
320
|
+
const projectData = project.state === "added" || project.state === "already" ? { path: project.path, added: project.state === "added" } : null;
|
|
321
|
+
const projectProblem = (() => {
|
|
322
|
+
switch (project.state) {
|
|
323
|
+
case "none": return "not inside a git repository";
|
|
324
|
+
case "refused": return project.message;
|
|
325
|
+
case "failed": return `${project.path} could not be added — ${project.message}`;
|
|
326
|
+
case "needs-yes": return `not added yet: run again with --yes to add ${project.path}`;
|
|
327
|
+
case "declined": return `${project.path} not added`;
|
|
328
|
+
default: return null;
|
|
329
|
+
}
|
|
330
|
+
})();
|
|
257
331
|
if (io.json) {
|
|
258
332
|
io.write(envelopeJson({
|
|
259
333
|
ok: true,
|
|
260
334
|
command,
|
|
261
|
-
project,
|
|
262
|
-
...(
|
|
335
|
+
project: projectData,
|
|
336
|
+
...(projectProblem === null ? {} : { projectProblem }),
|
|
263
337
|
agents: reports,
|
|
264
338
|
skill: skillData,
|
|
265
339
|
mcp,
|
|
@@ -267,9 +341,9 @@ export async function runOnboard(io) {
|
|
|
267
341
|
}));
|
|
268
342
|
return 0;
|
|
269
343
|
}
|
|
270
|
-
const projectLine =
|
|
271
|
-
? `project ${
|
|
272
|
-
:
|
|
344
|
+
const projectLine = projectData !== null
|
|
345
|
+
? `project ${projectData.path} — ${projectData.added ? "added" : "already added"}`
|
|
346
|
+
: project.state === "none" ? "project none added: run onboard inside the repository you want to hand off" : `project ${projectProblem}`;
|
|
273
347
|
io.write([
|
|
274
348
|
projectLine,
|
|
275
349
|
`agents ${reports.map(one => `${one.name}: ${[one.words, one.plan].filter(Boolean).join(" · ")}`).join("; ")}`,
|