@danypops/papyrus 0.44.9 → 0.44.10
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 +3 -2
- package/src/cli/artifact-command.ts +210 -0
- package/src/cli/discuss-command.ts +256 -0
- package/src/cli/docs-command.ts +192 -0
- package/src/cli/gates-command.ts +46 -0
- package/src/cli/graph-command.ts +171 -0
- package/src/cli/graph-projection-command.ts +79 -0
- package/src/cli/log-command.ts +101 -0
- package/src/cli/migration-command.ts +40 -0
- package/src/cli/note-command.ts +186 -0
- package/src/cli/playbooks-command.ts +313 -0
- package/src/cli/rules-command.ts +220 -0
- package/src/cli/session-identity-command.ts +58 -0
- package/src/cli/shared.ts +5 -0
- package/src/cli/stricli-run.ts +30 -0
- package/src/cli/task-command.ts +892 -0
- package/src/cli.ts +30 -1912
- package/src/handlers/discuss.ts +22 -20
- package/src/handlers/docs.ts +2 -28
- package/src/handlers/notes.ts +2 -29
- package/src/handlers/playbooks.ts +24 -90
- package/src/handlers/rules.ts +2 -28
- package/src/handlers/shared.ts +91 -1
- package/src/handlers/tasks.ts +31 -118
- package/src/modules/discuss.ts +5 -26
- package/src/modules/docs.ts +1 -22
- package/src/modules/graph-projection.ts +1 -8
- package/src/modules/logs.ts +1 -22
- package/src/modules/notes.ts +1 -22
- package/src/modules/operation-input.ts +35 -0
- package/src/modules/playbooks.ts +1 -22
- package/src/modules/rules.ts +1 -22
- package/src/modules/session-identity.ts +1 -15
- package/src/modules/tasks.ts +5 -33
- package/src/service.ts +1 -28
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import type { CommandContext } from "@stricli/core";
|
|
2
|
+
import { buildApplication, buildCommand, buildRouteMap, numberParser } from "@stricli/core";
|
|
3
|
+
import type { PapyrusClient } from "../client.ts";
|
|
4
|
+
import type { OperationName } from "../service.ts";
|
|
5
|
+
import { artifactLabel, type CliArtifact } from "./shared.ts";
|
|
6
|
+
import { runStricliToString } from "./stricli-run.ts";
|
|
7
|
+
|
|
8
|
+
type PlaybooksClient = Pick<PapyrusClient, "call">;
|
|
9
|
+
|
|
10
|
+
interface PlaybooksContext extends CommandContext {
|
|
11
|
+
readonly client: PlaybooksClient;
|
|
12
|
+
readonly json: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function parseStringArray(value: string): string[] {
|
|
16
|
+
const parsed = JSON.parse(value) as unknown;
|
|
17
|
+
if (!Array.isArray(parsed) || parsed.some((entry) => typeof entry !== "string")) throw new Error("value must be a JSON string array");
|
|
18
|
+
return parsed as string[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function parseObject(value: string): Record<string, unknown> {
|
|
22
|
+
const parsed = JSON.parse(value) as unknown;
|
|
23
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error("value must be a JSON object");
|
|
24
|
+
return parsed as Record<string, unknown>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* No further shape assertion here -- --arguments-json is genuinely polymorphic (an array on
|
|
29
|
+
* create, a {name: value} map on invoke) and --steps-json accepts a mix of plain prose strings
|
|
30
|
+
* and structured step objects that a single string-array assertion would wrongly reject. The
|
|
31
|
+
* service validates the real shape for whichever operation actually receives it. Typed as a
|
|
32
|
+
* concrete array-or-object union rather than bare `unknown` -- Stricli's flag-type inference
|
|
33
|
+
* doesn't correctly propagate a parser returning `unknown` through to the command function.
|
|
34
|
+
*/
|
|
35
|
+
function parseAny(value: string): unknown[] | Record<string, unknown> {
|
|
36
|
+
return JSON.parse(value) as unknown[] | Record<string, unknown>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function render(this: PlaybooksContext, result: unknown, human: string): void {
|
|
40
|
+
this.process.stdout.write(this.json ? JSON.stringify(result) : human);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const createCommand = buildCommand({
|
|
44
|
+
func: async function (
|
|
45
|
+
this: PlaybooksContext,
|
|
46
|
+
flags: {
|
|
47
|
+
title: string;
|
|
48
|
+
body?: string;
|
|
49
|
+
trigger?: string;
|
|
50
|
+
stepsJson?: unknown[] | Record<string, unknown>;
|
|
51
|
+
toolsJson?: string[];
|
|
52
|
+
labelsJson?: string[];
|
|
53
|
+
extraJson?: Record<string, unknown>;
|
|
54
|
+
argumentsJson?: unknown[] | Record<string, unknown>;
|
|
55
|
+
projectRoot?: string;
|
|
56
|
+
},
|
|
57
|
+
) {
|
|
58
|
+
const artifact = await this.client.call<Record<string, unknown>, CliArtifact>("playbooks.create", {
|
|
59
|
+
title: flags.title,
|
|
60
|
+
body: flags.body,
|
|
61
|
+
trigger: flags.trigger,
|
|
62
|
+
steps: flags.stepsJson,
|
|
63
|
+
tools: flags.toolsJson,
|
|
64
|
+
labels: flags.labelsJson,
|
|
65
|
+
extra: flags.extraJson,
|
|
66
|
+
arguments: flags.argumentsJson,
|
|
67
|
+
project_root: flags.projectRoot,
|
|
68
|
+
});
|
|
69
|
+
render.call(this, artifact, `Created playbook: ${artifactLabel(artifact)}`);
|
|
70
|
+
},
|
|
71
|
+
parameters: {
|
|
72
|
+
flags: {
|
|
73
|
+
title: { brief: "Playbook title", kind: "parsed", parse: String, placeholder: "text" },
|
|
74
|
+
body: { brief: "Playbook body", kind: "parsed", parse: String, placeholder: "text", optional: true },
|
|
75
|
+
trigger: { brief: "When this playbook applies", kind: "parsed", parse: String, placeholder: "text", optional: true },
|
|
76
|
+
stepsJson: {
|
|
77
|
+
brief: "JSON array of steps (strings and/or {kind:'doc'|'rule'|'call'|'task',...} objects)",
|
|
78
|
+
kind: "parsed",
|
|
79
|
+
parse: parseAny,
|
|
80
|
+
placeholder: "json",
|
|
81
|
+
optional: true,
|
|
82
|
+
},
|
|
83
|
+
toolsJson: { brief: "JSON string array of tool names", kind: "parsed", parse: parseStringArray, placeholder: "json", optional: true },
|
|
84
|
+
labelsJson: { brief: "JSON string array of labels", kind: "parsed", parse: parseStringArray, placeholder: "json", optional: true },
|
|
85
|
+
extraJson: { brief: "JSON object of extra fields", kind: "parsed", parse: parseObject, placeholder: "json", optional: true },
|
|
86
|
+
argumentsJson: {
|
|
87
|
+
brief: "JSON array of declared argument definitions",
|
|
88
|
+
kind: "parsed",
|
|
89
|
+
parse: parseAny,
|
|
90
|
+
placeholder: "json",
|
|
91
|
+
optional: true,
|
|
92
|
+
},
|
|
93
|
+
projectRoot: { brief: "Project scope", kind: "parsed", parse: String, placeholder: "path", optional: true },
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
docs: { brief: "Create a Playbook" },
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const listCommand = buildCommand({
|
|
100
|
+
func: async function (this: PlaybooksContext, flags: { status?: string; text?: string; limit?: number; projectRoot?: string }) {
|
|
101
|
+
const rows = await this.client.call<Record<string, unknown>, CliArtifact[]>("playbooks.list", {
|
|
102
|
+
status: flags.status,
|
|
103
|
+
text: flags.text,
|
|
104
|
+
limit: flags.limit,
|
|
105
|
+
project_root: flags.projectRoot,
|
|
106
|
+
});
|
|
107
|
+
render.call(this, rows, rows.length === 0 ? "No playbooks found." : rows.map((row) => artifactLabel(row)).join("\n"));
|
|
108
|
+
},
|
|
109
|
+
parameters: {
|
|
110
|
+
flags: {
|
|
111
|
+
status: { brief: "Filter by status", kind: "parsed", parse: String, placeholder: "status", optional: true },
|
|
112
|
+
text: { brief: "Substring match against title/body", kind: "parsed", parse: String, placeholder: "text", optional: true },
|
|
113
|
+
limit: { brief: "Maximum playbooks to return", kind: "parsed", parse: numberParser, optional: true },
|
|
114
|
+
projectRoot: { brief: "Project scope", kind: "parsed", parse: String, placeholder: "path", optional: true },
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
docs: { brief: "List Playbooks" },
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const showCommand = buildCommand({
|
|
121
|
+
func: async function (this: PlaybooksContext, _flags: Record<string, never>, id: string) {
|
|
122
|
+
const artifact = await this.client.call<Record<string, unknown>, CliArtifact>("playbooks.show", { id });
|
|
123
|
+
render.call(this, artifact, `${artifactLabel(artifact)}\n\n${artifact.body ?? ""}`);
|
|
124
|
+
},
|
|
125
|
+
parameters: { flags: {}, positional: { kind: "tuple", parameters: [{ brief: "Playbook id", parse: String, placeholder: "id" }] } },
|
|
126
|
+
docs: { brief: "Show one Playbook" },
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const previewCommand = buildCommand({
|
|
130
|
+
func: async function (this: PlaybooksContext, flags: { argumentsJson?: unknown[] | Record<string, unknown> }, id: string) {
|
|
131
|
+
const rendered = await this.client.call<Record<string, unknown>, string>("playbooks.preview", { id, arguments: flags.argumentsJson });
|
|
132
|
+
render.call(this, rendered, rendered);
|
|
133
|
+
},
|
|
134
|
+
parameters: {
|
|
135
|
+
flags: {
|
|
136
|
+
argumentsJson: {
|
|
137
|
+
brief: "JSON object supplying argument values",
|
|
138
|
+
kind: "parsed",
|
|
139
|
+
parse: parseAny,
|
|
140
|
+
placeholder: "json",
|
|
141
|
+
optional: true,
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
positional: { kind: "tuple", parameters: [{ brief: "Playbook id", parse: String, placeholder: "id" }] },
|
|
145
|
+
},
|
|
146
|
+
docs: { brief: "Render a Playbook's whole composition tree as text, creating nothing" },
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
const invokeCommand = buildCommand({
|
|
150
|
+
func: async function (
|
|
151
|
+
this: PlaybooksContext,
|
|
152
|
+
flags: { argumentsJson?: unknown[] | Record<string, unknown>; runId?: string; projectRoot?: string },
|
|
153
|
+
id: string,
|
|
154
|
+
) {
|
|
155
|
+
const invocation = await this.client.call<Record<string, unknown>, { entryTaskId: string; missingArguments?: string[] }>(
|
|
156
|
+
"playbooks.invoke",
|
|
157
|
+
{ id, arguments: flags.argumentsJson, run_id: flags.runId, project_root: flags.projectRoot },
|
|
158
|
+
);
|
|
159
|
+
render.call(
|
|
160
|
+
this,
|
|
161
|
+
invocation,
|
|
162
|
+
invocation.missingArguments
|
|
163
|
+
? `Missing required argument(s): ${invocation.missingArguments.join(", ")}.`
|
|
164
|
+
: `Invoked: entry task ${invocation.entryTaskId} focused. Drive it forward with \`tasks start/submit/complete\` like any other task.`,
|
|
165
|
+
);
|
|
166
|
+
},
|
|
167
|
+
parameters: {
|
|
168
|
+
flags: {
|
|
169
|
+
argumentsJson: {
|
|
170
|
+
brief: "JSON object supplying argument values",
|
|
171
|
+
kind: "parsed",
|
|
172
|
+
parse: parseAny,
|
|
173
|
+
placeholder: "json",
|
|
174
|
+
optional: true,
|
|
175
|
+
},
|
|
176
|
+
runId: { brief: "Run id to associate with this invocation", kind: "parsed", parse: String, placeholder: "id", optional: true },
|
|
177
|
+
projectRoot: { brief: "Project scope", kind: "parsed", parse: String, placeholder: "path", optional: true },
|
|
178
|
+
},
|
|
179
|
+
positional: { kind: "tuple", parameters: [{ brief: "Playbook id", parse: String, placeholder: "id" }] },
|
|
180
|
+
},
|
|
181
|
+
docs: { brief: "Compile a Playbook's steps into real Tasks and focus the first one" },
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
function buildEnableDisableCommand(action: "enable" | "disable") {
|
|
185
|
+
return buildCommand({
|
|
186
|
+
func: async function (this: PlaybooksContext, _flags: Record<string, never>, id: string) {
|
|
187
|
+
const artifact = await this.client.call<Record<string, unknown>, CliArtifact>(`playbooks.${action}` as OperationName, { id });
|
|
188
|
+
render.call(this, artifact, artifactLabel(artifact));
|
|
189
|
+
},
|
|
190
|
+
parameters: { flags: {}, positional: { kind: "tuple", parameters: [{ brief: "Playbook id", parse: String, placeholder: "id" }] } },
|
|
191
|
+
docs: { brief: `${action[0]!.toUpperCase()}${action.slice(1)} a Playbook` },
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const assignProjectCommand = buildCommand({
|
|
196
|
+
func: async function (this: PlaybooksContext, _flags: Record<string, never>, id: string, projectRoot?: string) {
|
|
197
|
+
const artifact = await this.client.call<Record<string, unknown>, CliArtifact>("playbooks.assign_project", {
|
|
198
|
+
id,
|
|
199
|
+
project_root: projectRoot,
|
|
200
|
+
});
|
|
201
|
+
render.call(this, artifact, projectRoot ? `Assigned ${id} to ${projectRoot}` : `Unscoped ${id}`);
|
|
202
|
+
},
|
|
203
|
+
parameters: {
|
|
204
|
+
flags: {},
|
|
205
|
+
positional: {
|
|
206
|
+
kind: "tuple",
|
|
207
|
+
parameters: [
|
|
208
|
+
{ brief: "Playbook id", parse: String, placeholder: "id" },
|
|
209
|
+
{ brief: "New project scope, omit to unscope", parse: String, placeholder: "project-root", optional: true },
|
|
210
|
+
],
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
docs: { brief: "Reassign a Playbook's project scope, or unscope it" },
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
const updateCommand = buildCommand({
|
|
217
|
+
func: async function (this: PlaybooksContext, flags: { title?: string; body?: string; labelsJson?: string[] }, id: string) {
|
|
218
|
+
if (flags.title === undefined && flags.body === undefined && flags.labelsJson === undefined)
|
|
219
|
+
throw new Error("playbooks update requires --title, --body, or --labels-json");
|
|
220
|
+
const artifact = await this.client.call<Record<string, unknown>, CliArtifact>("playbooks.update", {
|
|
221
|
+
id,
|
|
222
|
+
title: flags.title,
|
|
223
|
+
body: flags.body,
|
|
224
|
+
labels: flags.labelsJson,
|
|
225
|
+
});
|
|
226
|
+
render.call(this, artifact, artifactLabel(artifact));
|
|
227
|
+
},
|
|
228
|
+
parameters: {
|
|
229
|
+
flags: {
|
|
230
|
+
title: { brief: "New title", kind: "parsed", parse: String, placeholder: "text", optional: true },
|
|
231
|
+
body: { brief: "New body", kind: "parsed", parse: String, placeholder: "text", optional: true },
|
|
232
|
+
labelsJson: { brief: "JSON string array of labels", kind: "parsed", parse: parseStringArray, placeholder: "json", optional: true },
|
|
233
|
+
},
|
|
234
|
+
positional: { kind: "tuple", parameters: [{ brief: "Playbook id", parse: String, placeholder: "id" }] },
|
|
235
|
+
},
|
|
236
|
+
docs: { brief: "Change a Playbook's title/body/labels" },
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
function buildPairedIdCommand(
|
|
240
|
+
operation: OperationName,
|
|
241
|
+
fields: readonly [string, string],
|
|
242
|
+
human: (artifact: CliArtifact, second: string) => string,
|
|
243
|
+
brief: string,
|
|
244
|
+
) {
|
|
245
|
+
return buildCommand({
|
|
246
|
+
func: async function (this: PlaybooksContext, _flags: Record<string, never>, first: string, second: string) {
|
|
247
|
+
const artifact = await this.client.call<Record<string, unknown>, CliArtifact>(operation, {
|
|
248
|
+
[fields[0]]: first,
|
|
249
|
+
[fields[1]]: second,
|
|
250
|
+
});
|
|
251
|
+
render.call(this, artifact, human(artifact, second));
|
|
252
|
+
},
|
|
253
|
+
parameters: {
|
|
254
|
+
flags: {},
|
|
255
|
+
positional: {
|
|
256
|
+
kind: "tuple",
|
|
257
|
+
parameters: [
|
|
258
|
+
{ brief: "First playbook id", parse: String, placeholder: "id" },
|
|
259
|
+
{ brief: "Second playbook id", parse: String, placeholder: "id" },
|
|
260
|
+
],
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
docs: { brief },
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const app = buildApplication(
|
|
268
|
+
buildRouteMap({
|
|
269
|
+
routes: {
|
|
270
|
+
create: createCommand,
|
|
271
|
+
list: listCommand,
|
|
272
|
+
show: showCommand,
|
|
273
|
+
preview: previewCommand,
|
|
274
|
+
invoke: invokeCommand,
|
|
275
|
+
enable: buildEnableDisableCommand("enable"),
|
|
276
|
+
disable: buildEnableDisableCommand("disable"),
|
|
277
|
+
"assign-project": assignProjectCommand,
|
|
278
|
+
update: updateCommand,
|
|
279
|
+
contain: buildPairedIdCommand(
|
|
280
|
+
"playbooks.contain",
|
|
281
|
+
["parent_id", "child_id"],
|
|
282
|
+
(artifact, second) => `Nested: ${second} → ${artifactLabel(artifact)}`,
|
|
283
|
+
"Nest a child Playbook inside a parent",
|
|
284
|
+
),
|
|
285
|
+
uncontain: buildPairedIdCommand(
|
|
286
|
+
"playbooks.uncontain",
|
|
287
|
+
["parent_id", "child_id"],
|
|
288
|
+
(artifact, second) => `Removed ${second} from ${artifactLabel(artifact)}`,
|
|
289
|
+
"Remove a parent/child Playbook nesting",
|
|
290
|
+
),
|
|
291
|
+
depend: buildPairedIdCommand(
|
|
292
|
+
"playbooks.depend",
|
|
293
|
+
["id", "dependency_id"],
|
|
294
|
+
(artifact, second) => `Dependency added: ${artifactLabel(artifact)} waits for ${second}`,
|
|
295
|
+
"Chain a prerequisite Playbook before another",
|
|
296
|
+
),
|
|
297
|
+
undepend: buildPairedIdCommand(
|
|
298
|
+
"playbooks.undepend",
|
|
299
|
+
["id", "dependency_id"],
|
|
300
|
+
(artifact, second) => `Dependency removed: ${artifactLabel(artifact)} no longer waits for ${second}`,
|
|
301
|
+
"Remove a Playbook dependency",
|
|
302
|
+
),
|
|
303
|
+
},
|
|
304
|
+
docs: { brief: "Playbook operations" },
|
|
305
|
+
}),
|
|
306
|
+
{ name: "playbooks", scanner: { caseStyle: "allow-kebab-for-camel" } },
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
export async function runPlaybooksCli(args: string[], client: PlaybooksClient): Promise<string> {
|
|
310
|
+
const json = args.includes("--json");
|
|
311
|
+
const positional = args.filter((arg) => arg !== "--json");
|
|
312
|
+
return runStricliToString(app, positional, { client, json });
|
|
313
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import type { CommandContext } from "@stricli/core";
|
|
2
|
+
import { buildApplication, buildCommand, buildRouteMap, numberParser } from "@stricli/core";
|
|
3
|
+
import type { PapyrusClient } from "../client.ts";
|
|
4
|
+
import type { OperationName } from "../service.ts";
|
|
5
|
+
import { artifactLabel, type CliArtifact } from "./shared.ts";
|
|
6
|
+
import { runStricliToString } from "./stricli-run.ts";
|
|
7
|
+
|
|
8
|
+
type RulesClient = Pick<PapyrusClient, "call">;
|
|
9
|
+
|
|
10
|
+
interface RulesContext extends CommandContext {
|
|
11
|
+
readonly client: RulesClient;
|
|
12
|
+
readonly json: boolean;
|
|
13
|
+
/** The caller's own project root -- distinct from any --project-root flag; only `injectable` uses this. */
|
|
14
|
+
readonly callerProjectRoot: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function parseStringArray(value: string): string[] {
|
|
18
|
+
const parsed = JSON.parse(value) as unknown;
|
|
19
|
+
if (!Array.isArray(parsed) || parsed.some((entry) => typeof entry !== "string")) throw new Error("value must be a JSON string array");
|
|
20
|
+
return parsed as string[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function parseObject(value: string): Record<string, unknown> {
|
|
24
|
+
const parsed = JSON.parse(value) as unknown;
|
|
25
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error("value must be a JSON object");
|
|
26
|
+
return parsed as Record<string, unknown>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function render(this: RulesContext, result: unknown, human: string): void {
|
|
30
|
+
this.process.stdout.write(this.json ? JSON.stringify(result) : human);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const createCommand = buildCommand({
|
|
34
|
+
func: async function (
|
|
35
|
+
this: RulesContext,
|
|
36
|
+
flags: {
|
|
37
|
+
title: string;
|
|
38
|
+
body?: string;
|
|
39
|
+
condition?: string;
|
|
40
|
+
ruleAction?: string;
|
|
41
|
+
severity?: string;
|
|
42
|
+
labelsJson?: string[];
|
|
43
|
+
extraJson?: Record<string, unknown>;
|
|
44
|
+
projectRoot?: string;
|
|
45
|
+
},
|
|
46
|
+
) {
|
|
47
|
+
const artifact = await this.client.call<Record<string, unknown>, CliArtifact>("rules.create", {
|
|
48
|
+
title: flags.title,
|
|
49
|
+
body: flags.body,
|
|
50
|
+
condition: flags.condition,
|
|
51
|
+
rule_action: flags.ruleAction,
|
|
52
|
+
severity: flags.severity,
|
|
53
|
+
labels: flags.labelsJson,
|
|
54
|
+
extra: flags.extraJson,
|
|
55
|
+
project_root: flags.projectRoot,
|
|
56
|
+
});
|
|
57
|
+
render.call(this, artifact, `Created rule: ${artifactLabel(artifact)}`);
|
|
58
|
+
},
|
|
59
|
+
parameters: {
|
|
60
|
+
flags: {
|
|
61
|
+
title: { brief: "Rule title", kind: "parsed", parse: String, placeholder: "text" },
|
|
62
|
+
body: { brief: "Rule body", kind: "parsed", parse: String, placeholder: "text", optional: true },
|
|
63
|
+
condition: { brief: "When this rule applies", kind: "parsed", parse: String, placeholder: "text", optional: true },
|
|
64
|
+
ruleAction: { brief: "What the rule requires", kind: "parsed", parse: String, placeholder: "text", optional: true },
|
|
65
|
+
severity: { brief: "block|warn|info", kind: "parsed", parse: String, placeholder: "severity", optional: true },
|
|
66
|
+
labelsJson: { brief: "JSON string array of labels", kind: "parsed", parse: parseStringArray, placeholder: "json", optional: true },
|
|
67
|
+
extraJson: { brief: "JSON object of extra fields", kind: "parsed", parse: parseObject, placeholder: "json", optional: true },
|
|
68
|
+
projectRoot: { brief: "Project scope", kind: "parsed", parse: String, placeholder: "path", optional: true },
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
docs: { brief: "Create a Rule" },
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const listCommand = buildCommand({
|
|
75
|
+
func: async function (this: RulesContext, flags: { status?: string; text?: string; limit?: number; projectRoot?: string }) {
|
|
76
|
+
const rows = await this.client.call<Record<string, unknown>, CliArtifact[]>("rules.list", {
|
|
77
|
+
status: flags.status,
|
|
78
|
+
text: flags.text,
|
|
79
|
+
limit: flags.limit,
|
|
80
|
+
project_root: flags.projectRoot,
|
|
81
|
+
});
|
|
82
|
+
render.call(this, rows, rows.length === 0 ? "No rules found." : rows.map((row) => artifactLabel(row)).join("\n"));
|
|
83
|
+
},
|
|
84
|
+
parameters: {
|
|
85
|
+
flags: {
|
|
86
|
+
status: { brief: "Filter by status", kind: "parsed", parse: String, placeholder: "status", optional: true },
|
|
87
|
+
text: { brief: "Substring match against title/body", kind: "parsed", parse: String, placeholder: "text", optional: true },
|
|
88
|
+
limit: { brief: "Maximum rules to return", kind: "parsed", parse: numberParser, optional: true },
|
|
89
|
+
projectRoot: { brief: "Project scope", kind: "parsed", parse: String, placeholder: "path", optional: true },
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
docs: { brief: "List Rules" },
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const assignProjectCommand = buildCommand({
|
|
96
|
+
func: async function (this: RulesContext, _flags: Record<string, never>, id: string, projectRoot?: string) {
|
|
97
|
+
const artifact = await this.client.call<Record<string, unknown>, CliArtifact>("rules.assign_project", {
|
|
98
|
+
id,
|
|
99
|
+
project_root: projectRoot,
|
|
100
|
+
});
|
|
101
|
+
render.call(this, artifact, projectRoot ? `Assigned ${id} to ${projectRoot}` : `Unscoped ${id}`);
|
|
102
|
+
},
|
|
103
|
+
parameters: {
|
|
104
|
+
flags: {},
|
|
105
|
+
positional: {
|
|
106
|
+
kind: "tuple",
|
|
107
|
+
parameters: [
|
|
108
|
+
{ brief: "Rule id", parse: String, placeholder: "id" },
|
|
109
|
+
{ brief: "New project scope, omit to unscope", parse: String, placeholder: "project-root", optional: true },
|
|
110
|
+
],
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
docs: { brief: "Reassign a Rule's project scope, or unscope it" },
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const showCommand = buildCommand({
|
|
117
|
+
func: async function (this: RulesContext, _flags: Record<string, never>, id: string) {
|
|
118
|
+
const artifact = await this.client.call<Record<string, unknown>, CliArtifact>("rules.show", { id });
|
|
119
|
+
render.call(this, artifact, `${artifactLabel(artifact)}\n\n${artifact.body ?? ""}`);
|
|
120
|
+
},
|
|
121
|
+
parameters: { flags: {}, positional: { kind: "tuple", parameters: [{ brief: "Rule id", parse: String, placeholder: "id" }] } },
|
|
122
|
+
docs: { brief: "Show one Rule" },
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const previewCommand = buildCommand({
|
|
126
|
+
func: async function (this: RulesContext, _flags: Record<string, never>, id: string) {
|
|
127
|
+
const preview = await this.client.call<Record<string, unknown>, string>("rules.preview", { id });
|
|
128
|
+
render.call(this, preview, preview);
|
|
129
|
+
},
|
|
130
|
+
parameters: { flags: {}, positional: { kind: "tuple", parameters: [{ brief: "Rule id", parse: String, placeholder: "id" }] } },
|
|
131
|
+
docs: { brief: "Render a Rule's own condition/action/body preview text" },
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
function buildEnableDisableCommand(action: "enable" | "disable") {
|
|
135
|
+
return buildCommand({
|
|
136
|
+
func: async function (this: RulesContext, _flags: Record<string, never>, id: string) {
|
|
137
|
+
const artifact = await this.client.call<Record<string, unknown>, CliArtifact>(`rules.${action}` as OperationName, { id });
|
|
138
|
+
render.call(this, artifact, artifactLabel(artifact));
|
|
139
|
+
},
|
|
140
|
+
parameters: { flags: {}, positional: { kind: "tuple", parameters: [{ brief: "Rule id", parse: String, placeholder: "id" }] } },
|
|
141
|
+
docs: { brief: `${action[0]!.toUpperCase()}${action.slice(1)} a Rule` },
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const gateCommand = buildCommand({
|
|
146
|
+
func: async function (this: RulesContext, _flags: Record<string, never>, id: string, taskId: string) {
|
|
147
|
+
const artifact = await this.client.call<Record<string, unknown>, CliArtifact>("rules.gate", { id, task_id: taskId });
|
|
148
|
+
render.call(this, artifact, `Gated ${taskId} with rule ${artifactLabel(artifact)}`);
|
|
149
|
+
},
|
|
150
|
+
parameters: {
|
|
151
|
+
flags: {},
|
|
152
|
+
positional: {
|
|
153
|
+
kind: "tuple",
|
|
154
|
+
parameters: [
|
|
155
|
+
{ brief: "Rule id", parse: String, placeholder: "rule-id" },
|
|
156
|
+
{ brief: "Task id to gate", parse: String, placeholder: "task-id" },
|
|
157
|
+
],
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
docs: { brief: "Attach a Rule as a gate condition on a Task" },
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
const injectableCommand = buildCommand({
|
|
164
|
+
func: async function (this: RulesContext) {
|
|
165
|
+
const rows = await this.client.call<Record<string, unknown>, CliArtifact[]>("rules.injectable", {
|
|
166
|
+
project_root: this.callerProjectRoot,
|
|
167
|
+
});
|
|
168
|
+
render.call(this, rows, rows.length === 0 ? "No injectable rules." : rows.map((row) => row.title).join("\n"));
|
|
169
|
+
},
|
|
170
|
+
parameters: { flags: {} },
|
|
171
|
+
docs: { brief: "List Rules currently injectable into the agent system prompt for this project" },
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const updateCommand = buildCommand({
|
|
175
|
+
func: async function (this: RulesContext, flags: { title?: string; body?: string; labelsJson?: string[] }, id: string) {
|
|
176
|
+
if (flags.title === undefined && flags.body === undefined && flags.labelsJson === undefined)
|
|
177
|
+
throw new Error("rules update requires --title, --body, or --labels-json");
|
|
178
|
+
const artifact = await this.client.call<Record<string, unknown>, CliArtifact>("rules.update", {
|
|
179
|
+
id,
|
|
180
|
+
title: flags.title,
|
|
181
|
+
body: flags.body,
|
|
182
|
+
labels: flags.labelsJson,
|
|
183
|
+
});
|
|
184
|
+
render.call(this, artifact, artifactLabel(artifact));
|
|
185
|
+
},
|
|
186
|
+
parameters: {
|
|
187
|
+
flags: {
|
|
188
|
+
title: { brief: "New title", kind: "parsed", parse: String, placeholder: "text", optional: true },
|
|
189
|
+
body: { brief: "New body", kind: "parsed", parse: String, placeholder: "text", optional: true },
|
|
190
|
+
labelsJson: { brief: "JSON string array of labels", kind: "parsed", parse: parseStringArray, placeholder: "json", optional: true },
|
|
191
|
+
},
|
|
192
|
+
positional: { kind: "tuple", parameters: [{ brief: "Rule id", parse: String, placeholder: "id" }] },
|
|
193
|
+
},
|
|
194
|
+
docs: { brief: "Change a Rule's title/body/labels" },
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
const app = buildApplication(
|
|
198
|
+
buildRouteMap({
|
|
199
|
+
routes: {
|
|
200
|
+
create: createCommand,
|
|
201
|
+
list: listCommand,
|
|
202
|
+
"assign-project": assignProjectCommand,
|
|
203
|
+
show: showCommand,
|
|
204
|
+
preview: previewCommand,
|
|
205
|
+
enable: buildEnableDisableCommand("enable"),
|
|
206
|
+
disable: buildEnableDisableCommand("disable"),
|
|
207
|
+
gate: gateCommand,
|
|
208
|
+
injectable: injectableCommand,
|
|
209
|
+
update: updateCommand,
|
|
210
|
+
},
|
|
211
|
+
docs: { brief: "Rule operations" },
|
|
212
|
+
}),
|
|
213
|
+
{ name: "rules", scanner: { caseStyle: "allow-kebab-for-camel" } },
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
export async function runRulesCli(args: string[], client: RulesClient, projectRoot: string = process.cwd()): Promise<string> {
|
|
217
|
+
const json = args.includes("--json");
|
|
218
|
+
const positional = args.filter((arg) => arg !== "--json");
|
|
219
|
+
return runStricliToString(app, positional, { client, json, callerProjectRoot: projectRoot });
|
|
220
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { CommandContext } from "@stricli/core";
|
|
2
|
+
import { buildApplication, buildCommand, buildRouteMap } from "@stricli/core";
|
|
3
|
+
import type { PapyrusClient } from "../client.ts";
|
|
4
|
+
import { runStricliToString } from "./stricli-run.ts";
|
|
5
|
+
|
|
6
|
+
type SessionIdentityClient = Pick<PapyrusClient, "call">;
|
|
7
|
+
|
|
8
|
+
interface SessionIdentityContext extends CommandContext {
|
|
9
|
+
readonly client: SessionIdentityClient;
|
|
10
|
+
readonly json: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function render(this: SessionIdentityContext, result: unknown): void {
|
|
14
|
+
this.process.stdout.write(this.json ? JSON.stringify(result) : JSON.stringify(result, null, 2));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const registerCommand = buildCommand({
|
|
18
|
+
func: async function (this: SessionIdentityContext, flags: { sessionId: string }) {
|
|
19
|
+
const result = await this.client.call<Record<string, unknown>, { sessionId: string; secret: string }>("session.register", {
|
|
20
|
+
session_id: flags.sessionId,
|
|
21
|
+
});
|
|
22
|
+
render.call(this, result);
|
|
23
|
+
},
|
|
24
|
+
parameters: {
|
|
25
|
+
flags: {
|
|
26
|
+
sessionId: { brief: "Session id to register", kind: "parsed", parse: String, placeholder: "id" },
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
docs: { brief: "Register a session identity, receiving back a secret for later mutations" },
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const releaseCommand = buildCommand({
|
|
33
|
+
func: async function (this: SessionIdentityContext, flags: { sessionId: string; sessionSecret?: string }) {
|
|
34
|
+
const result = await this.client.call<Record<string, unknown>, { released: boolean }>("session.release", {
|
|
35
|
+
session_id: flags.sessionId,
|
|
36
|
+
...(flags.sessionSecret ? { session_secret: flags.sessionSecret } : {}),
|
|
37
|
+
});
|
|
38
|
+
render.call(this, result);
|
|
39
|
+
},
|
|
40
|
+
parameters: {
|
|
41
|
+
flags: {
|
|
42
|
+
sessionId: { brief: "Session id to release", kind: "parsed", parse: String, placeholder: "id" },
|
|
43
|
+
sessionSecret: { brief: "Secret returned at registration", kind: "parsed", parse: String, placeholder: "secret", optional: true },
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
docs: { brief: "Release a registered session identity" },
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const app = buildApplication(
|
|
50
|
+
buildRouteMap({ routes: { register: registerCommand, release: releaseCommand }, docs: { brief: "Session identity operations" } }),
|
|
51
|
+
{ name: "session", scanner: { caseStyle: "allow-kebab-for-camel" } },
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
export async function runSessionIdentityCli(args: string[], client: SessionIdentityClient): Promise<string> {
|
|
55
|
+
const json = args.includes("--json");
|
|
56
|
+
const positional = args.filter((arg) => arg !== "--json");
|
|
57
|
+
return runStricliToString(app, positional, { client, json });
|
|
58
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Application, CommandContext } from "@stricli/core";
|
|
2
|
+
import { run } from "@stricli/core";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Every run*Cli export keeps the pre-Stricli (args, client) => Promise<string> contract so
|
|
6
|
+
* test/cli-parity.test.ts and cli.ts's main() dispatch don't need to change per migrated command.
|
|
7
|
+
* Stricli itself never throws for a user-facing failure (invalid flag, unknown route) -- it
|
|
8
|
+
* writes to context.process.stderr and sets context.process.exitCode instead -- so this captures
|
|
9
|
+
* both in memory and converts a nonzero exit code back into a thrown Error to match every other
|
|
10
|
+
* run*Cli function's own contract.
|
|
11
|
+
*/
|
|
12
|
+
export async function runStricliToString<CONTEXT extends CommandContext>(
|
|
13
|
+
app: Application<CONTEXT>,
|
|
14
|
+
args: string[],
|
|
15
|
+
contextWithoutProcess: Omit<CONTEXT, "process">,
|
|
16
|
+
): Promise<string> {
|
|
17
|
+
const chunks: string[] = [];
|
|
18
|
+
const errors: string[] = [];
|
|
19
|
+
const process: {
|
|
20
|
+
stdout: { write: (text: string) => void };
|
|
21
|
+
stderr: { write: (text: string) => void };
|
|
22
|
+
exitCode?: number | string | null;
|
|
23
|
+
} = {
|
|
24
|
+
stdout: { write: (text: string) => chunks.push(text) },
|
|
25
|
+
stderr: { write: (text: string) => errors.push(text) },
|
|
26
|
+
};
|
|
27
|
+
await run(app, args, { ...contextWithoutProcess, process } as CONTEXT);
|
|
28
|
+
if (process.exitCode) throw new Error(errors.join("").trim() || `command failed with exit code ${process.exitCode}`);
|
|
29
|
+
return chunks.join("");
|
|
30
|
+
}
|