@danypops/papyrus 0.44.9 → 0.44.11
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/artifact/artifact.ts +23 -0
- 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 +4 -30
- package/src/handlers/notes.ts +2 -29
- package/src/handlers/playbooks.ts +27 -92
- package/src/handlers/rules.ts +4 -30
- package/src/handlers/shared.ts +92 -1
- package/src/handlers/tasks.ts +31 -118
- package/src/modules/discuss.ts +5 -26
- package/src/modules/docs.ts +6 -23
- 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 +42 -0
- package/src/modules/playbooks.ts +6 -23
- package/src/modules/rules.ts +6 -23
- package/src/modules/session-identity.ts +1 -15
- package/src/modules/tasks.ts +5 -33
- package/src/service.ts +1 -28
|
@@ -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
|
+
}
|