@ccmsg/cli 0.2.12 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/cli.ts +96 -29
- package/src/daemon/registry.ts +41 -14
- package/src/daemon/supervise.ts +2 -1
- package/src/harness/index.ts +96 -0
- package/src/instance/config.ts +18 -0
- package/src/instance/instance.ts +25 -5
- package/src/instance/paths.ts +32 -6
- package/src/messaging/direct.ts +116 -0
- package/src/plugin/claude.ts +1 -72
- package/src/plugin/codex.ts +332 -0
- package/src/plugin/index.ts +7 -5
- package/src/plugin/install.ts +41 -161
- package/src/plugin/receipt.ts +202 -0
- package/src/plugin/skill.ts +84 -0
- package/src/sessions/harness.ts +166 -31
- package/src/sessions/registry.ts +75 -53
- package/src/sessions/search.ts +4 -4
- package/src/transcript/files.ts +103 -22
- package/src/transcript/fold.ts +72 -1
package/src/plugin/install.ts
CHANGED
|
@@ -1,24 +1,24 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { rm } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
3
|
import type { InstancePaths } from "../instance/index.ts";
|
|
4
4
|
import { claudePluginFiles, MARKETPLACE_NAME, PLUGIN_ID } from "./claude.ts";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
5
|
+
import * as codex from "./codex.ts";
|
|
6
|
+
import {
|
|
7
|
+
type Agent,
|
|
8
|
+
type InstallReport,
|
|
9
|
+
place,
|
|
10
|
+
type Ran,
|
|
11
|
+
readReceipt,
|
|
12
|
+
type Receipt,
|
|
13
|
+
receiptFile,
|
|
14
|
+
refusal as refusalOf,
|
|
15
|
+
type Refusal,
|
|
16
|
+
rootFor,
|
|
17
|
+
type Run,
|
|
18
|
+
type StatusReport,
|
|
19
|
+
type UninstallReport,
|
|
20
|
+
writeReceipt,
|
|
21
|
+
} from "./receipt.ts";
|
|
22
22
|
|
|
23
23
|
export const runClaude: Run = async (args) => {
|
|
24
24
|
let spawned: Bun.Subprocess<"ignore", "pipe", "pipe">;
|
|
@@ -34,140 +34,28 @@ export const runClaude: Run = async (args) => {
|
|
|
34
34
|
return { code: await spawned.exited, stdout, stderr };
|
|
35
35
|
};
|
|
36
36
|
|
|
37
|
-
/**
|
|
38
|
-
* refused, and what it said. */
|
|
39
|
-
export interface Refusal {
|
|
40
|
-
readonly command: readonly string[];
|
|
41
|
-
readonly code: number;
|
|
42
|
-
readonly said: string;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/** What the three commands answer with.
|
|
37
|
+
/** The three commands, dispatched to the agent they are about.
|
|
46
38
|
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
export interface InstallReport extends Report {
|
|
60
|
-
readonly version: string;
|
|
61
|
-
readonly config_home: string;
|
|
62
|
-
readonly root: string;
|
|
63
|
-
/** The files laid down, by their path under `root`. */
|
|
64
|
-
readonly files: readonly string[];
|
|
65
|
-
readonly marketplace: { readonly name: string; readonly registered: boolean };
|
|
66
|
-
readonly plugin: {
|
|
67
|
-
readonly id: string;
|
|
68
|
-
readonly installed: boolean;
|
|
69
|
-
/** Whether a copy of the same id was taken out first, which is what makes
|
|
70
|
-
* a repeated install run what was just laid down. */
|
|
71
|
-
readonly replaced: boolean;
|
|
72
|
-
};
|
|
73
|
-
/** The agent commands that were run, as they were run. */
|
|
74
|
-
readonly commands: readonly (readonly string[])[];
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export interface StatusReport extends Report {
|
|
78
|
-
/** Where the receipt is. Absent when ccmsg installed nothing here, which is
|
|
79
|
-
* what makes every field below it absent too. */
|
|
80
|
-
readonly receipt?: string;
|
|
81
|
-
readonly installed_at?: string;
|
|
82
|
-
/** What the receipt says was installed. */
|
|
83
|
-
readonly version?: string;
|
|
84
|
-
readonly config_home?: string;
|
|
85
|
-
readonly root?: string;
|
|
86
|
-
/** The receipt's files, counted against what is under `root` now. */
|
|
87
|
-
readonly files?: {
|
|
88
|
-
readonly expected: number;
|
|
89
|
-
readonly present: number;
|
|
90
|
-
readonly missing: readonly string[];
|
|
91
|
-
};
|
|
92
|
-
readonly marketplace: {
|
|
93
|
-
readonly name?: string;
|
|
94
|
-
/** Whether the agent has it. Absent when the agent could not be asked,
|
|
95
|
-
* which is a different thing from it not being registered. */
|
|
96
|
-
readonly registered?: boolean;
|
|
97
|
-
/** Where the agent thinks it points, when that is not where the receipt
|
|
98
|
-
* put it. */
|
|
99
|
-
readonly points_at?: string;
|
|
100
|
-
};
|
|
101
|
-
readonly plugin: {
|
|
102
|
-
readonly id?: string;
|
|
103
|
-
/** What the agent reports having, and whether it has it switched on.
|
|
104
|
-
* Present with no `expected_version` beside it means something other than
|
|
105
|
-
* ccmsg installed it. */
|
|
106
|
-
readonly installed_version?: string;
|
|
107
|
-
readonly enabled?: boolean;
|
|
108
|
-
/** What the receipt says should be there. */
|
|
109
|
-
readonly expected_version?: string;
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
export interface UninstallReport extends Report {
|
|
114
|
-
readonly receipt?: string;
|
|
115
|
-
/** What was actually taken back out. A step the receipt does not name was
|
|
116
|
-
* never taken, so it is not undone and does not appear here. */
|
|
117
|
-
readonly removed: {
|
|
118
|
-
readonly plugin?: string;
|
|
119
|
-
readonly marketplace?: string;
|
|
120
|
-
readonly root?: string;
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
export type Outcome = InstallReport | StatusReport | UninstallReport;
|
|
125
|
-
|
|
126
|
-
/** What one install did, so that uninstall can undo exactly that.
|
|
127
|
-
*
|
|
128
|
-
* Everything reversible is written down before the next step is taken: the
|
|
129
|
-
* files that were laid down, the commands that were run against the agent, and
|
|
130
|
-
* the id the agent now knows the plugin by. Undoing reads this and nothing
|
|
131
|
-
* else — an install that half-finished leaves a receipt for the half that
|
|
132
|
-
* happened, and a plugin somebody else installed is not in it and is left
|
|
133
|
-
* alone. */
|
|
134
|
-
export interface Receipt {
|
|
135
|
-
readonly agent: Agent;
|
|
136
|
-
readonly version: string;
|
|
137
|
-
readonly installed_at: string;
|
|
138
|
-
/** The config home the agent was asked to install into. */
|
|
139
|
-
readonly config_home: string;
|
|
140
|
-
/** Where the plugin's own files were laid down. */
|
|
141
|
-
readonly root: string;
|
|
142
|
-
/** Their paths under that root, in the order they were written. */
|
|
143
|
-
readonly files: readonly string[];
|
|
144
|
-
/** The agent commands that were run, as they were run. */
|
|
145
|
-
readonly commands: readonly (readonly string[])[];
|
|
146
|
-
readonly marketplace?: string;
|
|
147
|
-
readonly plugin_id?: string;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function rootFor(paths: InstancePaths, agent: Agent): string {
|
|
151
|
-
return join(paths.pluginsDir, agent);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
function receiptFile(paths: InstancePaths, agent: Agent): string {
|
|
155
|
-
return join(paths.pluginsDir, `${agent}.receipt.json`);
|
|
39
|
+
* What each one installs differs in kind — Claude Code takes a plugin through
|
|
40
|
+
* its own CLI, Codex reads files out of its config home — so the two are
|
|
41
|
+
* written apart and only the shapes they answer with are shared. */
|
|
42
|
+
export function install(
|
|
43
|
+
paths: InstancePaths,
|
|
44
|
+
agent: Agent,
|
|
45
|
+
version: string,
|
|
46
|
+
run?: Run,
|
|
47
|
+
): Promise<InstallReport> {
|
|
48
|
+
return agent === "codex"
|
|
49
|
+
? codex.install(paths, version, run)
|
|
50
|
+
: installClaude(paths, version, run);
|
|
156
51
|
}
|
|
157
52
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
const parsed: unknown = JSON.parse(await readFile(receiptFile(paths, agent), "utf8"));
|
|
161
|
-
return typeof parsed === "object" && parsed !== null ? (parsed as Receipt) : undefined;
|
|
162
|
-
} catch {
|
|
163
|
-
return undefined;
|
|
164
|
-
}
|
|
53
|
+
export function status(paths: InstancePaths, agent: Agent, run?: Run): Promise<StatusReport> {
|
|
54
|
+
return agent === "codex" ? codex.status(paths, run) : statusClaude(paths, run);
|
|
165
55
|
}
|
|
166
56
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
await mkdir(dirname(file), { recursive: true });
|
|
170
|
-
await writeFile(file, `${JSON.stringify(receipt, null, 2)}\n`);
|
|
57
|
+
export function uninstall(paths: InstancePaths, agent: Agent, run?: Run): Promise<UninstallReport> {
|
|
58
|
+
return agent === "codex" ? codex.uninstall(paths) : uninstallClaude(paths, run);
|
|
171
59
|
}
|
|
172
60
|
|
|
173
61
|
/** Lay the plugin's files down under the instance's own state, register it
|
|
@@ -180,21 +68,14 @@ async function writeReceipt(paths: InstancePaths, receipt: Receipt): Promise<voi
|
|
|
180
68
|
* and a version already installed is not re-read. So an install that finds its
|
|
181
69
|
* own id already there removes it first, which is what makes "install" mean
|
|
182
70
|
* "what is running is what was just laid down". */
|
|
183
|
-
|
|
71
|
+
async function installClaude(
|
|
184
72
|
paths: InstancePaths,
|
|
185
73
|
version: string,
|
|
186
74
|
run: Run = runClaude,
|
|
187
75
|
): Promise<InstallReport> {
|
|
188
76
|
const root = rootFor(paths, "claude");
|
|
189
77
|
const files = claudePluginFiles(version);
|
|
190
|
-
|
|
191
|
-
const file = join(root, path);
|
|
192
|
-
await mkdir(dirname(file), { recursive: true });
|
|
193
|
-
await writeFile(
|
|
194
|
-
file,
|
|
195
|
-
typeof content === "string" ? content : `${JSON.stringify(content, null, 2)}\n`,
|
|
196
|
-
);
|
|
197
|
-
}
|
|
78
|
+
await place(root, files);
|
|
198
79
|
|
|
199
80
|
// Kept as it grows rather than rebuilt per step: each step adds what it did
|
|
200
81
|
// to what the earlier ones did, and the file on disk is that running total.
|
|
@@ -254,7 +135,7 @@ export async function install(
|
|
|
254
135
|
* the current reading of the same thing beside it, so drift — a file deleted, a
|
|
255
136
|
* marketplace pointed elsewhere, a version other than the one installed — is
|
|
256
137
|
* two fields that differ rather than a sentence about them. */
|
|
257
|
-
|
|
138
|
+
async function statusClaude(paths: InstancePaths, run: Run = runClaude): Promise<StatusReport> {
|
|
258
139
|
const receipt = await readReceipt(paths, "claude");
|
|
259
140
|
const here = await installedRow(run);
|
|
260
141
|
const registered = await marketplaces(run);
|
|
@@ -325,7 +206,7 @@ function known(
|
|
|
325
206
|
* then of the marketplace that offered it, and only then are the files it was
|
|
326
207
|
* reading taken away. A step the receipt does not name is a step that was
|
|
327
208
|
* never taken, so it is not undone. */
|
|
328
|
-
|
|
209
|
+
async function uninstallClaude(
|
|
329
210
|
paths: InstancePaths,
|
|
330
211
|
run: Run = runClaude,
|
|
331
212
|
): Promise<UninstallReport> {
|
|
@@ -411,6 +292,5 @@ function rows(output: string): Record<string, unknown>[] {
|
|
|
411
292
|
* exited with, and its first line of complaint. The exit code is stated apart
|
|
412
293
|
* from the words because a command that said nothing still failed. */
|
|
413
294
|
function refusal(command: readonly string[], ran: Ran): Refusal {
|
|
414
|
-
|
|
415
|
-
return { command: ["claude", ...command], code: ran.code, said: said.split("\n")[0] ?? "" };
|
|
295
|
+
return refusalOf("claude", command, ran);
|
|
416
296
|
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { HARNESSES } from "../harness/index.ts";
|
|
4
|
+
import type { InstancePaths } from "../instance/index.ts";
|
|
5
|
+
|
|
6
|
+
/** The agents ccmsg can install a plugin for, which are the harnesses it
|
|
7
|
+
* speaks to: what is installed is how a session of that harness reaches this
|
|
8
|
+
* instance, so there is one plugin per harness and no third thing to name. */
|
|
9
|
+
export const AGENTS = HARNESSES;
|
|
10
|
+
export type Agent = (typeof AGENTS)[number];
|
|
11
|
+
|
|
12
|
+
/** How an agent's own CLI is run. The environment is inherited, which is how
|
|
13
|
+
* the install lands in the config home this instance answers for and not in
|
|
14
|
+
* another one (M6). Named so a test can watch what would be run without a
|
|
15
|
+
* config home of a person's being touched. */
|
|
16
|
+
export type Run = (args: readonly string[]) => Promise<Ran>;
|
|
17
|
+
|
|
18
|
+
export interface Ran {
|
|
19
|
+
readonly code: number;
|
|
20
|
+
readonly stdout: string;
|
|
21
|
+
readonly stderr: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Why one of these commands stopped where it did: the agent command that was
|
|
25
|
+
* refused, and what it said. */
|
|
26
|
+
export interface Refusal {
|
|
27
|
+
readonly command: readonly string[];
|
|
28
|
+
readonly code: number;
|
|
29
|
+
readonly said: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** A command the agent refused, as the report carries it: what was run, what it
|
|
33
|
+
* exited with, and its first line of complaint. The exit code is stated apart
|
|
34
|
+
* from the words because a command that said nothing still failed. */
|
|
35
|
+
export function refusal(binary: string, command: readonly string[], ran: Ran): Refusal {
|
|
36
|
+
const said = `${ran.stderr}${ran.stdout}`.trim();
|
|
37
|
+
return { command: [binary, ...command], code: ran.code, said: said.split("\n")[0] ?? "" };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** What the three commands answer with.
|
|
41
|
+
*
|
|
42
|
+
* Fields rather than sentences: these commands are read by whatever runs them
|
|
43
|
+
* as much as by a person, and a line of prose is something a caller has to
|
|
44
|
+
* parse back into the facts it was built from. The words a person wants are in
|
|
45
|
+
* `--help`; what is here is what was found. */
|
|
46
|
+
export interface Report {
|
|
47
|
+
readonly agent: Agent;
|
|
48
|
+
/** Whether the command did everything it set out to do. */
|
|
49
|
+
readonly ok: boolean;
|
|
50
|
+
/** The step that stopped it. Absent while `ok`. */
|
|
51
|
+
readonly refused?: Refusal;
|
|
52
|
+
/** What the agent still needs a person to do before what was installed takes
|
|
53
|
+
* effect. Absent where nothing does. */
|
|
54
|
+
readonly needs?: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface InstallReport extends Report {
|
|
58
|
+
readonly version: string;
|
|
59
|
+
readonly config_home: string;
|
|
60
|
+
readonly root: string;
|
|
61
|
+
/** The files laid down, by their path under `root`. */
|
|
62
|
+
readonly files: readonly string[];
|
|
63
|
+
/** The files laid down elsewhere, by their whole path. What an agent reads
|
|
64
|
+
* out of its own config home rather than out of a plugin's directory goes
|
|
65
|
+
* here, so uninstall takes back exactly what was put there. */
|
|
66
|
+
readonly placed?: readonly string[];
|
|
67
|
+
readonly marketplace?: { readonly name: string; readonly registered: boolean };
|
|
68
|
+
readonly plugin?: {
|
|
69
|
+
readonly id: string;
|
|
70
|
+
readonly installed: boolean;
|
|
71
|
+
/** Whether a copy of the same id was taken out first, which is what makes
|
|
72
|
+
* a repeated install run what was just laid down. */
|
|
73
|
+
readonly replaced: boolean;
|
|
74
|
+
};
|
|
75
|
+
/** The agent commands that were run, as they were run. */
|
|
76
|
+
readonly commands: readonly (readonly string[])[];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface StatusReport extends Report {
|
|
80
|
+
/** Where the receipt is. Absent when ccmsg installed nothing here, which is
|
|
81
|
+
* what makes every field below it absent too. */
|
|
82
|
+
readonly receipt?: string;
|
|
83
|
+
readonly installed_at?: string;
|
|
84
|
+
/** What the receipt says was installed. */
|
|
85
|
+
readonly version?: string;
|
|
86
|
+
readonly config_home?: string;
|
|
87
|
+
readonly root?: string;
|
|
88
|
+
/** The receipt's files, counted against what is under `root` now. */
|
|
89
|
+
readonly files?: {
|
|
90
|
+
readonly expected: number;
|
|
91
|
+
readonly present: number;
|
|
92
|
+
readonly missing: readonly string[];
|
|
93
|
+
};
|
|
94
|
+
readonly marketplace?: {
|
|
95
|
+
readonly name?: string;
|
|
96
|
+
/** Whether the agent has it. Absent when the agent could not be asked,
|
|
97
|
+
* which is a different thing from it not being registered. */
|
|
98
|
+
readonly registered?: boolean;
|
|
99
|
+
/** Where the agent thinks it points, when that is not where the receipt
|
|
100
|
+
* put it. */
|
|
101
|
+
readonly points_at?: string;
|
|
102
|
+
};
|
|
103
|
+
readonly plugin?: {
|
|
104
|
+
readonly id?: string;
|
|
105
|
+
/** What the agent reports having, and whether it has it switched on.
|
|
106
|
+
* Present with no `expected_version` beside it means something other than
|
|
107
|
+
* ccmsg installed it. */
|
|
108
|
+
readonly installed_version?: string;
|
|
109
|
+
readonly enabled?: boolean;
|
|
110
|
+
/** What the receipt says should be there. */
|
|
111
|
+
readonly expected_version?: string;
|
|
112
|
+
};
|
|
113
|
+
/** Whether the agent's hooks are switched on at all, where that is a setting
|
|
114
|
+
* of the agent rather than of the plugin. Absent when it could not be asked. */
|
|
115
|
+
readonly hooks_enabled?: boolean;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export interface UninstallReport extends Report {
|
|
119
|
+
readonly receipt?: string;
|
|
120
|
+
/** What was actually taken back out. A step the receipt does not name was
|
|
121
|
+
* never taken, so it is not undone and does not appear here. */
|
|
122
|
+
readonly removed: {
|
|
123
|
+
readonly plugin?: string;
|
|
124
|
+
readonly marketplace?: string;
|
|
125
|
+
readonly root?: string;
|
|
126
|
+
readonly placed?: readonly string[];
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export type Outcome = InstallReport | StatusReport | UninstallReport;
|
|
131
|
+
|
|
132
|
+
/** What one install did, so that uninstall can undo exactly that.
|
|
133
|
+
*
|
|
134
|
+
* Everything reversible is written down before the next step is taken: the
|
|
135
|
+
* files that were laid down, the commands that were run against the agent, and
|
|
136
|
+
* the id the agent now knows the plugin by. Undoing reads this and nothing
|
|
137
|
+
* else — an install that half-finished leaves a receipt for the half that
|
|
138
|
+
* happened, and a plugin somebody else installed is not in it and is left
|
|
139
|
+
* alone. */
|
|
140
|
+
export interface Receipt {
|
|
141
|
+
readonly agent: Agent;
|
|
142
|
+
readonly version: string;
|
|
143
|
+
readonly installed_at: string;
|
|
144
|
+
/** The config home the agent was asked to install into. */
|
|
145
|
+
readonly config_home: string;
|
|
146
|
+
/** Where the plugin's own files were laid down. */
|
|
147
|
+
readonly root: string;
|
|
148
|
+
/** Their paths under that root, in the order they were written. */
|
|
149
|
+
readonly files: readonly string[];
|
|
150
|
+
/** Whole paths written outside that root, in the order they were written. */
|
|
151
|
+
readonly placed?: readonly string[];
|
|
152
|
+
/** The agent commands that were run, as they were run. */
|
|
153
|
+
readonly commands: readonly (readonly string[])[];
|
|
154
|
+
readonly marketplace?: string;
|
|
155
|
+
readonly plugin_id?: string;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function rootFor(paths: InstancePaths, agent: Agent): string {
|
|
159
|
+
return join(paths.pluginsDir, agent);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function receiptFile(paths: InstancePaths, agent: Agent): string {
|
|
163
|
+
return join(paths.pluginsDir, `${agent}.receipt.json`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export async function readReceipt(
|
|
167
|
+
paths: InstancePaths,
|
|
168
|
+
agent: Agent,
|
|
169
|
+
): Promise<Receipt | undefined> {
|
|
170
|
+
try {
|
|
171
|
+
const parsed: unknown = JSON.parse(await readFile(receiptFile(paths, agent), "utf8"));
|
|
172
|
+
return typeof parsed === "object" && parsed !== null ? (parsed as Receipt) : undefined;
|
|
173
|
+
} catch {
|
|
174
|
+
return undefined;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export async function writeReceipt(paths: InstancePaths, receipt: Receipt): Promise<void> {
|
|
179
|
+
const file = receiptFile(paths, receipt.agent);
|
|
180
|
+
await mkdir(dirname(file), { recursive: true });
|
|
181
|
+
await writeFile(file, `${JSON.stringify(receipt, null, 2)}\n`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Lay a set of files down, each under the root, in the order given. A string
|
|
185
|
+
* is written as it is; anything else is the content of a JSON file, so the
|
|
186
|
+
* definitions state shapes rather than text and one place turns a value into
|
|
187
|
+
* bytes. */
|
|
188
|
+
export async function place(
|
|
189
|
+
root: string,
|
|
190
|
+
files: ReadonlyMap<string, string | object>,
|
|
191
|
+
mode?: number,
|
|
192
|
+
): Promise<void> {
|
|
193
|
+
for (const [path, content] of files) {
|
|
194
|
+
const file = join(root, path);
|
|
195
|
+
await mkdir(dirname(file), { recursive: true });
|
|
196
|
+
await writeFile(
|
|
197
|
+
file,
|
|
198
|
+
typeof content === "string" ? content : `${JSON.stringify(content, null, 2)}\n`,
|
|
199
|
+
mode === undefined ? undefined : { mode },
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/** What a session is told about ccmsg, and how ccmsg is described where an
|
|
2
|
+
* agent lists what it has installed.
|
|
3
|
+
*
|
|
4
|
+
* One text for every harness: what a session has to know is how to answer a
|
|
5
|
+
* message and how to find somebody to send one to, and neither depends on
|
|
6
|
+
* which program the session runs in. The plugin around it differs — where the
|
|
7
|
+
* file goes, how a hook is declared — and the words do not.
|
|
8
|
+
*
|
|
9
|
+
* Written out rather than shipped as a file in the package: the daemon, the
|
|
10
|
+
* CLI and the plugin are one release, and generating the plugin from the
|
|
11
|
+
* running binary is what keeps the three from drifting into three versions of
|
|
12
|
+
* "what ccmsg is". */
|
|
13
|
+
|
|
14
|
+
export const DESCRIPTION = "別のセッションと行き来するメッセージ";
|
|
15
|
+
|
|
16
|
+
export const SKILL = `---
|
|
17
|
+
name: ccmsg
|
|
18
|
+
description: 別のセッションへ声をかける・届いたメッセージに返す・見ている人へ知らせる時に使う。
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
# ccmsg
|
|
22
|
+
|
|
23
|
+
同じ人が動かしている別のセッションと、メッセージをやり取りする。相手が別のハーネスで動いていても同じ手順で届く。
|
|
24
|
+
|
|
25
|
+
## 届いたメッセージに返す
|
|
26
|
+
|
|
27
|
+
メッセージは \`<cross-session-message>\` の封筒で届き、本文の最後に返信の一行が付いている。
|
|
28
|
+
|
|
29
|
+
\`\`\`
|
|
30
|
+
Reply with: ccmsg reply <mid> --to <sid> <text>
|
|
31
|
+
\`\`\`
|
|
32
|
+
|
|
33
|
+
**その行をそのまま実行する。** 宛先も、どのメッセージへの返事かも、その行が持っている。
|
|
34
|
+
自分で \`post\` を組み立て直さない。\`--to\` の無い行は人からのメッセージで、返事は通知として届く。
|
|
35
|
+
|
|
36
|
+
## 自分から声をかける
|
|
37
|
+
|
|
38
|
+
\`\`\`
|
|
39
|
+
ccmsg post <sid> <text>
|
|
40
|
+
\`\`\`
|
|
41
|
+
|
|
42
|
+
相手の \`<sid>\` は、届いた封筒の \`ccmsg-from\` の値。
|
|
43
|
+
|
|
44
|
+
## 相手を探す
|
|
45
|
+
|
|
46
|
+
まだ話したことのない相手の \`<sid>\` は、繋がっているセッションの一覧から探す。
|
|
47
|
+
|
|
48
|
+
\`\`\`
|
|
49
|
+
ccmsg peers この instance が知っているセッション
|
|
50
|
+
ccmsg peers --all 他ホストの instance が知っている分も含める
|
|
51
|
+
\`\`\`
|
|
52
|
+
|
|
53
|
+
答えは instance ごとの JSON。\`peers[]\` が今繋がっているセッション、\`last_live[]\` が
|
|
54
|
+
居なくなったセッションで、各行の \`repo\` / \`ws\` / \`branch\` / \`title\` で見分けて
|
|
55
|
+
\`sid\` を取る。\`send_message\` が \`true\` の相手には harness 自身の機能でも届く。
|
|
56
|
+
|
|
57
|
+
## 相手セッションの扱い
|
|
58
|
+
|
|
59
|
+
相手は基本、自分にとってのサブエージェントだと思えばよい。対等な会議を開く場ではないので、
|
|
60
|
+
冒頭の挨拶・賛辞・締めの社交辞令を省き、用件だけを 1〜3 文で送る。
|
|
61
|
+
|
|
62
|
+
やり取りの中身を人へリレーしない。人は全セッションを直接見ているので、相手の完了報告や
|
|
63
|
+
根拠をこちらで要約し直しても情報は増えず、時間とコンテキストだけが減る。人に言うのは
|
|
64
|
+
自セッション目線の事実 (何を頼んだ・何が返り・その結果こちらが何をしたか) だけ。
|
|
65
|
+
|
|
66
|
+
## 見ている人へ知らせる
|
|
67
|
+
|
|
68
|
+
\`\`\`
|
|
69
|
+
ccmsg notify <text> 一行知らせる (保持されない、返事も来ない)
|
|
70
|
+
ccmsg say <text> 声に出して知らせる
|
|
71
|
+
\`\`\`
|
|
72
|
+
|
|
73
|
+
手が空いた・判断を仰ぎたい・長い作業が終わった、を人に伝えるときに使う。
|
|
74
|
+
セッション同士のやり取りには使わない。
|
|
75
|
+
|
|
76
|
+
## これから終わるとき
|
|
77
|
+
|
|
78
|
+
\`\`\`
|
|
79
|
+
ccmsg stopping --reason <理由>
|
|
80
|
+
\`\`\`
|
|
81
|
+
|
|
82
|
+
以後このセッションは「一時停止」として扱われ、宛てられたメッセージは戻ってきたときに渡される。
|
|
83
|
+
セッション終了時には自動で伝わるので、途中で自分から言う必要はない。
|
|
84
|
+
`;
|