@danypops/papyrus 0.44.8 → 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.
Files changed (93) hide show
  1. package/package.json +3 -2
  2. package/src/{ports → artifact}/artifact-event-reader.ts +1 -1
  3. package/src/{artifact-relationship-view.ts → artifact/artifact-relationship-view.ts} +3 -3
  4. package/src/{ports → artifact}/artifact-store.ts +2 -2
  5. package/src/{artifact-subtree.ts → artifact/artifact-subtree.ts} +4 -4
  6. package/src/{ports → artifact}/artifact-trash-store.ts +2 -2
  7. package/src/{adapters → artifact}/sqlite-artifact-scope-store.ts +1 -1
  8. package/src/{adapters → artifact}/sqlite-artifact-store.ts +15 -15
  9. package/src/authority-registry.ts +3 -3
  10. package/src/cli/artifact-command.ts +210 -0
  11. package/src/cli/discuss-command.ts +256 -0
  12. package/src/cli/docs-command.ts +192 -0
  13. package/src/cli/gates-command.ts +46 -0
  14. package/src/cli/graph-command.ts +171 -0
  15. package/src/cli/graph-projection-command.ts +79 -0
  16. package/src/cli/log-command.ts +101 -0
  17. package/src/cli/migration-command.ts +40 -0
  18. package/src/cli/note-command.ts +186 -0
  19. package/src/cli/playbooks-command.ts +313 -0
  20. package/src/cli/rules-command.ts +220 -0
  21. package/src/cli/session-identity-command.ts +58 -0
  22. package/src/cli/shared.ts +5 -0
  23. package/src/cli/stricli-run.ts +30 -0
  24. package/src/cli/task-command.ts +892 -0
  25. package/src/cli.ts +32 -1914
  26. package/src/client.ts +2 -2
  27. package/src/{daemon-state.ts → daemon/daemon-state.ts} +1 -1
  28. package/src/{daemon.ts → daemon/daemon.ts} +3 -3
  29. package/src/db.ts +1 -1
  30. package/src/{discussion-service.ts → discussion/discussion-service.ts} +7 -6
  31. package/src/domain-services.ts +5 -5
  32. package/src/{graph-projection-service.ts → graph-projection/graph-projection-service.ts} +6 -6
  33. package/src/{vehicle/artifact-trash-vehicle.ts → handlers/artifact-trash.ts} +4 -4
  34. package/src/{vehicle/discuss-vehicle.ts → handlers/discuss.ts} +20 -25
  35. package/src/{vehicle/docs-vehicle.ts → handlers/docs.ts} +4 -37
  36. package/src/{vehicle/notes-vehicle.ts → handlers/notes.ts} +4 -38
  37. package/src/{vehicle/playbooks-vehicle.ts → handlers/playbooks.ts} +34 -100
  38. package/src/{vehicle/papyrus-vehicle.ts → handlers/registry.ts} +16 -16
  39. package/src/{vehicle/rules-vehicle.ts → handlers/rules.ts} +4 -37
  40. package/src/{vehicle/artifact-vehicle-shared.ts → handlers/shared.ts} +96 -6
  41. package/src/{vehicle/tasks-vehicle.ts → handlers/tasks.ts} +37 -124
  42. package/src/index.ts +13 -13
  43. package/src/{log-service.ts → log/log-service.ts} +2 -2
  44. package/src/modules/discuss.ts +7 -28
  45. package/src/modules/docs.ts +4 -24
  46. package/src/modules/graph-projection.ts +5 -11
  47. package/src/modules/logs.ts +2 -23
  48. package/src/modules/notes.ts +2 -23
  49. package/src/modules/operation-input.ts +35 -0
  50. package/src/modules/playbooks.ts +13 -33
  51. package/src/modules/rules.ts +4 -24
  52. package/src/modules/session-identity.ts +2 -16
  53. package/src/modules/tasks.ts +11 -38
  54. package/src/{note-service.ts → note/note-service.ts} +6 -6
  55. package/src/ops.ts +6 -6
  56. package/src/{playbook-definition.ts → playbook/playbook-definition.ts} +7 -6
  57. package/src/{playbook-execution.ts → playbook/playbook-execution.ts} +5 -4
  58. package/src/{workflow-execution.ts → playbook/workflow-execution.ts} +12 -12
  59. package/src/service.ts +30 -57
  60. package/src/{session-identity-service.ts → session-identity/session-identity-service.ts} +2 -2
  61. package/src/{ports → stores}/session-identity-store.ts +1 -1
  62. package/src/{adapters → stores}/sqlite-discussion-round-store.ts +1 -1
  63. package/src/{adapters → stores}/sqlite-gate-runner.ts +1 -1
  64. package/src/{adapters → stores}/sqlite-graph-projection-store.ts +1 -1
  65. package/src/{adapters → stores}/sqlite-log-store.ts +1 -1
  66. package/src/{adapters → stores}/sqlite-note-event-store.ts +1 -1
  67. package/src/{adapters → stores}/sqlite-session-identity-store.ts +1 -1
  68. package/src/{adapters → stores}/sqlite-task-event-store.ts +1 -1
  69. package/src/{adapters → stores}/sqlite-task-focus-store.ts +1 -1
  70. package/src/{adapters → stores}/sqlite-task-lease-store.ts +1 -1
  71. package/src/{adapters → stores}/sqlite-task-scope-store.ts +1 -1
  72. package/src/{task-context.ts → task/task-context.ts} +4 -4
  73. package/src/{task-execution.ts → task/task-execution.ts} +1 -1
  74. package/src/{task-graph-view.ts → task/task-graph-view.ts} +1 -1
  75. package/src/{task-relationship-view.ts → task/task-relationship-view.ts} +3 -3
  76. package/src/{task-service.ts → task/task-service.ts} +14 -14
  77. /package/src/{domain → artifact}/artifact-alias.ts +0 -0
  78. /package/src/{domain → artifact}/artifact-event.ts +0 -0
  79. /package/src/{ports → artifact}/artifact-scope-store.ts +0 -0
  80. /package/src/{domain → artifact}/artifact-trash.ts +0 -0
  81. /package/src/{domain → artifact}/artifact.ts +0 -0
  82. /package/src/{ports → artifact}/atomic-artifact-store.ts +0 -0
  83. /package/src/{log.ts → log/log.ts} +0 -0
  84. /package/src/{ports → stores}/discussion-round-store.ts +0 -0
  85. /package/src/{ports → stores}/gate-runner.ts +0 -0
  86. /package/src/{ports → stores}/graph-projection-store.ts +0 -0
  87. /package/src/{ports → stores}/graph-renderer.ts +0 -0
  88. /package/src/{ports → stores}/log-store.ts +0 -0
  89. /package/src/{ports → stores}/note-event-store.ts +0 -0
  90. /package/src/{ports → stores}/task-event-store.ts +0 -0
  91. /package/src/{ports → stores}/task-focus-store.ts +0 -0
  92. /package/src/{ports → stores}/task-lease-store.ts +0 -0
  93. /package/src/{ports → stores}/task-scope-store.ts +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danypops/papyrus",
3
- "version": "0.44.8",
3
+ "version": "0.44.10",
4
4
  "description": "Daemon-backed graph artifacts, evidence-bearing tasks, rules, skills, and native TUI workflows for Pi",
5
5
  "type": "module",
6
6
  "keywords": ["pi-package"],
@@ -36,6 +36,7 @@
36
36
  "dependencies": {
37
37
  "@danypops/vehicle-client": "^0.6.1",
38
38
  "@danypops/vehicle-core": "^0.12.3",
39
- "@danypops/vehicle-server": "^0.18.1"
39
+ "@danypops/vehicle-server": "^0.18.1",
40
+ "@stricli/core": "^1.3.0"
40
41
  }
41
42
  }
@@ -1,4 +1,4 @@
1
- import type { ArtifactEventPage, ArtifactEventQuery } from "../domain/artifact-event.ts";
1
+ import type { ArtifactEventPage, ArtifactEventQuery } from "./artifact-event.ts";
2
2
 
3
3
  /** Read access to the generic mutation event log shared by every kind -- split out of
4
4
  * ArtifactStore since only service.ts's graph.history operation reads it. */
@@ -1,6 +1,6 @@
1
- import type { Artifact } from "./domain/artifact.ts";
2
- import type { DisplayGraph, DisplayGraphEdge, DisplayGraphNode } from "./domain/display-graph.ts";
3
- import { fallbackLabel } from "./task-relationship-view.ts";
1
+ import type { DisplayGraph, DisplayGraphEdge, DisplayGraphNode } from "../domain/display-graph.ts";
2
+ import { fallbackLabel } from "../task/task-relationship-view.ts";
3
+ import type { Artifact } from "./artifact.ts";
4
4
 
5
5
  /**
6
6
  * The generic-artifact counterpart to projectTaskRelationships: unlike a Task, which already
@@ -7,8 +7,8 @@ import type {
7
7
  CreateArtifactInput,
8
8
  RelationshipQuery,
9
9
  UpdateArtifactInput,
10
- } from "../domain/artifact.ts";
11
- import type { ArtifactEventContext } from "../domain/artifact-event.ts";
10
+ } from "./artifact.ts";
11
+ import type { ArtifactEventContext } from "./artifact-event.ts";
12
12
 
13
13
  /** Core CRUD/graph operations every domain-service module needs. Trash lifecycle and event-log
14
14
  * reading are separate ports (ArtifactTrashStore, ArtifactEventReader) -- only the composition
@@ -5,10 +5,10 @@
5
5
  * Tasks.cancelSubtree's traversal shape but performs trash(), not a lifecycle transition,
6
6
  * so it applies to any kind that participates in `contains` (task, playbook), not just Task.
7
7
  */
8
- import { ARTIFACT_REMOVE_SUBTREE_MAX_NODES } from "./constants.ts";
9
- import type { ArtifactEventContext } from "./domain/artifact-event.ts";
10
- import type { ArtifactStore } from "./ports/artifact-store.ts";
11
- import type { ArtifactTrashStore } from "./ports/artifact-trash-store.ts";
8
+ import { ARTIFACT_REMOVE_SUBTREE_MAX_NODES } from "../constants.ts";
9
+ import type { ArtifactEventContext } from "./artifact-event.ts";
10
+ import type { ArtifactStore } from "./artifact-store.ts";
11
+ import type { ArtifactTrashStore } from "./artifact-trash-store.ts";
12
12
 
13
13
  export interface RemoveSubtreeResult {
14
14
  removed: string[];
@@ -1,5 +1,5 @@
1
- import type { ArtifactEventContext } from "../domain/artifact-event.ts";
2
- import type { ArtifactTrashRecord } from "../domain/artifact-trash.ts";
1
+ import type { ArtifactEventContext } from "./artifact-event.ts";
2
+ import type { ArtifactTrashRecord } from "./artifact-trash.ts";
3
3
 
4
4
  /** Trash lifecycle for artifacts -- split out of ArtifactStore since only the composition root
5
5
  * (daemon.ts, service.ts) needs it; every domain-service module depends on core CRUD/graph only. */
@@ -1,7 +1,7 @@
1
1
  import type { Db } from "../db.ts";
2
2
  import { inTransaction } from "../db.ts";
3
3
  import type { TaskScopeSource } from "../domain/task-scope.ts";
4
- import type { ArtifactScope, ArtifactScopeStore } from "../ports/artifact-scope-store.ts";
4
+ import type { ArtifactScope, ArtifactScopeStore } from "./artifact-scope-store.ts";
5
5
 
6
6
  export class SQLiteArtifactScopeStore implements ArtifactScopeStore {
7
7
  constructor(private readonly db: Db) {}
@@ -1,17 +1,5 @@
1
1
  import type { Db } from "../db.ts";
2
2
  import { inTransaction } from "../db.ts";
3
- import type {
4
- Artifact,
5
- ArtifactEdge,
6
- ArtifactGraphOptions,
7
- ArtifactLink,
8
- ArtifactQuery,
9
- CreateArtifactInput,
10
- RelationshipQuery,
11
- UpdateArtifactInput,
12
- } from "../domain/artifact.ts";
13
- import type { ArtifactEventContext, ArtifactEventPage, ArtifactEventQuery } from "../domain/artifact-event.ts";
14
- import type { ArtifactTrashRecord } from "../domain/artifact-trash.ts";
15
3
  import {
16
4
  createArtifact,
17
5
  getArtifact,
@@ -29,9 +17,21 @@ import {
29
17
  updateExtra,
30
18
  updateStatus,
31
19
  } from "../ops.ts";
32
- import type { ArtifactEventReader } from "../ports/artifact-event-reader.ts";
33
- import type { ArtifactTrashStore } from "../ports/artifact-trash-store.ts";
34
- import type { AtomicArtifactStore } from "../ports/atomic-artifact-store.ts";
20
+ import type {
21
+ Artifact,
22
+ ArtifactEdge,
23
+ ArtifactGraphOptions,
24
+ ArtifactLink,
25
+ ArtifactQuery,
26
+ CreateArtifactInput,
27
+ RelationshipQuery,
28
+ UpdateArtifactInput,
29
+ } from "./artifact.ts";
30
+ import type { ArtifactEventContext, ArtifactEventPage, ArtifactEventQuery } from "./artifact-event.ts";
31
+ import type { ArtifactEventReader } from "./artifact-event-reader.ts";
32
+ import type { ArtifactTrashRecord } from "./artifact-trash.ts";
33
+ import type { ArtifactTrashStore } from "./artifact-trash-store.ts";
34
+ import type { AtomicArtifactStore } from "./atomic-artifact-store.ts";
35
35
 
36
36
  export class SQLiteArtifactStore implements AtomicArtifactStore, ArtifactTrashStore, ArtifactEventReader {
37
37
  constructor(private readonly db: Db) {}
@@ -19,9 +19,9 @@
19
19
  * already lives, matching "the core has generic registries" from the decision doc.
20
20
  */
21
21
 
22
- import type { Artifact, ArtifactLink } from "./domain/artifact.ts";
23
- import type { ArtifactEventContext } from "./domain/artifact-event.ts";
24
- import type { ArtifactStore } from "./ports/artifact-store.ts";
22
+ import type { Artifact, ArtifactLink } from "./artifact/artifact.ts";
23
+ import type { ArtifactEventContext } from "./artifact/artifact-event.ts";
24
+ import type { ArtifactStore } from "./artifact/artifact-store.ts";
25
25
 
26
26
  export type ArtifactAction = "create" | "link" | "status" | "update";
27
27
 
@@ -0,0 +1,210 @@
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 { artifactLabel, type CliArtifact } from "./shared.ts";
5
+ import { runStricliToString } from "./stricli-run.ts";
6
+
7
+ type ArtifactClient = Pick<PapyrusClient, "call">;
8
+
9
+ interface ArtifactContext extends CommandContext {
10
+ readonly client: ArtifactClient;
11
+ readonly json: boolean;
12
+ readonly projectRoot: string;
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
+ function render(this: ArtifactContext, result: unknown, human: string): void {
28
+ this.process.stdout.write(this.json ? JSON.stringify(result) : human);
29
+ }
30
+
31
+ const createCommand = buildCommand({
32
+ func: async function (
33
+ this: ArtifactContext,
34
+ flags: {
35
+ kind?: string;
36
+ title?: string;
37
+ body?: string;
38
+ status?: string;
39
+ subtype?: string;
40
+ labelsJson?: string[];
41
+ extraJson?: Record<string, unknown>;
42
+ templateId?: string;
43
+ },
44
+ ) {
45
+ if (!flags.kind && !flags.templateId) throw new Error("artifact create requires --kind (or --template-id)");
46
+ const artifact = await this.client.call<Record<string, unknown>, CliArtifact>("artifact.create", {
47
+ kind: flags.kind,
48
+ title: flags.title,
49
+ body: flags.body,
50
+ status: flags.status,
51
+ subtype: flags.subtype,
52
+ labels: flags.labelsJson,
53
+ extra: flags.extraJson,
54
+ template_id: flags.templateId,
55
+ ...(flags.kind === "task" ? { project_root: this.projectRoot } : {}),
56
+ });
57
+ render.call(this, artifact, `Created: ${artifactLabel(artifact)}`);
58
+ },
59
+ parameters: {
60
+ flags: {
61
+ kind: { brief: "Artifact kind", kind: "parsed", parse: String, placeholder: "kind", optional: true },
62
+ title: { brief: "Title", kind: "parsed", parse: String, placeholder: "text", optional: true },
63
+ body: { brief: "Body", kind: "parsed", parse: String, placeholder: "text", optional: true },
64
+ status: { brief: "Initial status", kind: "parsed", parse: String, placeholder: "status", optional: true },
65
+ subtype: { brief: "Subtype", kind: "parsed", parse: String, placeholder: "text", 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
+ templateId: { brief: "Template to instantiate from", kind: "parsed", parse: String, placeholder: "id", optional: true },
69
+ },
70
+ },
71
+ docs: { brief: "Create an artifact of any kind" },
72
+ });
73
+
74
+ const queryCommand = buildCommand({
75
+ func: async function (this: ArtifactContext, flags: { kind?: string; status?: string; text?: string; limit?: number }) {
76
+ const rows = await this.client.call<Record<string, unknown>, CliArtifact[]>("artifact.query", {
77
+ kind: flags.kind,
78
+ status: flags.status,
79
+ text: flags.text,
80
+ limit: flags.limit,
81
+ });
82
+ render.call(this, rows, rows.length === 0 ? "No artifacts found." : rows.map((row) => artifactLabel(row)).join("\n"));
83
+ },
84
+ parameters: {
85
+ flags: {
86
+ kind: { brief: "Filter by kind", kind: "parsed", parse: String, placeholder: "kind", optional: true },
87
+ status: { brief: "Filter by status", kind: "parsed", parse: String, placeholder: "status", optional: true },
88
+ text: { brief: "Substring match against title/body", kind: "parsed", parse: String, placeholder: "text", optional: true },
89
+ limit: { brief: "Maximum artifacts to return", kind: "parsed", parse: numberParser, optional: true },
90
+ },
91
+ },
92
+ docs: { brief: "Query artifacts across every kind" },
93
+ });
94
+
95
+ const showCommand = buildCommand({
96
+ func: async function (this: ArtifactContext, flags: { depth?: number; maxNodes?: number }, id: string) {
97
+ const artifact = await this.client.call<Record<string, unknown>, CliArtifact>("artifact.show", {
98
+ id,
99
+ depth: flags.depth,
100
+ max_nodes: flags.maxNodes,
101
+ });
102
+ render.call(this, artifact, `${artifactLabel(artifact)}\n\n${artifact.body ?? ""}`);
103
+ },
104
+ parameters: {
105
+ flags: {
106
+ depth: { brief: "Edge traversal depth", kind: "parsed", parse: numberParser, optional: true },
107
+ maxNodes: { brief: "Maximum traversed nodes", kind: "parsed", parse: numberParser, optional: true },
108
+ },
109
+ positional: { kind: "tuple", parameters: [{ brief: "Artifact id", parse: String, placeholder: "id" }] },
110
+ },
111
+ docs: { brief: "Show one artifact by id, regardless of kind" },
112
+ });
113
+
114
+ const removeCommand = buildCommand({
115
+ func: async function (this: ArtifactContext, flags: { reason?: string }, id: string) {
116
+ const record = await this.client.call<
117
+ Record<string, unknown>,
118
+ { artifactId: string; trashedAt: string; purgeAfter: string; reason?: string }
119
+ >("artifact.remove", { id, reason: flags.reason });
120
+ render.call(this, record, `Trashed ${record.artifactId}: eligible for purge at ${record.purgeAfter}`);
121
+ },
122
+ parameters: {
123
+ flags: { reason: { brief: "Why this artifact was removed", kind: "parsed", parse: String, placeholder: "text", optional: true } },
124
+ positional: { kind: "tuple", parameters: [{ brief: "Artifact id", parse: String, placeholder: "id" }] },
125
+ },
126
+ docs: { brief: "Move an artifact to a time-gated trash" },
127
+ });
128
+
129
+ const removeSubtreeCommand = buildCommand({
130
+ func: async function (this: ArtifactContext, flags: { reason?: string }, id: string) {
131
+ const outcome = await this.client.call<Record<string, unknown>, { removed: string[]; skipped: string[] }>("artifact.remove_subtree", {
132
+ id,
133
+ reason: flags.reason,
134
+ });
135
+ render.call(
136
+ this,
137
+ outcome,
138
+ `Trashed ${outcome.removed.length} artifact(s)${outcome.skipped.length > 0 ? `, skipped ${outcome.skipped.length} already-trashed` : ""}.`,
139
+ );
140
+ },
141
+ parameters: {
142
+ flags: { reason: { brief: "Why this subtree was removed", kind: "parsed", parse: String, placeholder: "text", optional: true } },
143
+ positional: { kind: "tuple", parameters: [{ brief: "Artifact id", parse: String, placeholder: "id" }] },
144
+ },
145
+ docs: { brief: "Trash an artifact and its whole contains subtree" },
146
+ });
147
+
148
+ const restoreCommand = buildCommand({
149
+ func: async function (this: ArtifactContext, _flags: Record<string, never>, id: string) {
150
+ const outcome = await this.client.call<Record<string, unknown>, { restored: boolean }>("artifact.restore", { id });
151
+ render.call(this, outcome, outcome.restored ? `Restored ${id}` : `${id} was not trashed`);
152
+ },
153
+ parameters: { flags: {}, positional: { kind: "tuple", parameters: [{ brief: "Artifact id", parse: String, placeholder: "id" }] } },
154
+ docs: { brief: "Restore a trashed artifact" },
155
+ });
156
+
157
+ const trashStatusCommand = buildCommand({
158
+ func: async function (this: ArtifactContext, _flags: Record<string, never>, id: string) {
159
+ const record = await this.client.call<
160
+ Record<string, unknown>,
161
+ { artifactId: string; trashedAt: string; purgeAfter: string; reason?: string } | null
162
+ >("artifact.trash_status", { id });
163
+ render.call(
164
+ this,
165
+ record,
166
+ record ? `${record.artifactId}: trashed at ${record.trashedAt}, purge eligible at ${record.purgeAfter}` : `${id} is not trashed`,
167
+ );
168
+ },
169
+ parameters: { flags: {}, positional: { kind: "tuple", parameters: [{ brief: "Artifact id", parse: String, placeholder: "id" }] } },
170
+ docs: { brief: "Show one artifact's trash status" },
171
+ });
172
+
173
+ const trashListCommand = buildCommand({
174
+ func: async function (this: ArtifactContext) {
175
+ const rows = await this.client.call<
176
+ Record<string, unknown>,
177
+ Array<{ artifactId: string; trashedAt: string; purgeAfter: string; reason?: string }>
178
+ >("artifact.trash_list", {});
179
+ render.call(
180
+ this,
181
+ rows,
182
+ rows.length === 0 ? "Trash is empty." : rows.map((row) => `${row.artifactId}: purge eligible at ${row.purgeAfter}`).join("\n"),
183
+ );
184
+ },
185
+ parameters: { flags: {} },
186
+ docs: { brief: "List every trashed artifact" },
187
+ });
188
+
189
+ const app = buildApplication(
190
+ buildRouteMap({
191
+ routes: {
192
+ create: createCommand,
193
+ query: queryCommand,
194
+ show: showCommand,
195
+ remove: removeCommand,
196
+ "remove-subtree": removeSubtreeCommand,
197
+ restore: restoreCommand,
198
+ "trash-status": trashStatusCommand,
199
+ "trash-list": trashListCommand,
200
+ },
201
+ docs: { brief: "Artifact operations" },
202
+ }),
203
+ { name: "artifact", scanner: { caseStyle: "allow-kebab-for-camel" } },
204
+ );
205
+
206
+ export async function runArtifactCli(args: string[], client: ArtifactClient, projectRoot: string = process.cwd()): Promise<string> {
207
+ const json = args.includes("--json");
208
+ const positional = args.filter((arg) => arg !== "--json");
209
+ return runStricliToString(app, positional, { client, json, projectRoot });
210
+ }
@@ -0,0 +1,256 @@
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 { runStricliToString } from "./stricli-run.ts";
6
+
7
+ type DiscussClient = Pick<PapyrusClient, "call">;
8
+
9
+ interface DiscussContext extends CommandContext {
10
+ readonly client: DiscussClient;
11
+ readonly json: boolean;
12
+ }
13
+
14
+ function parseStringArray(value: string): string[] {
15
+ const parsed = JSON.parse(value) as unknown;
16
+ if (!Array.isArray(parsed) || parsed.some((entry) => typeof entry !== "string")) throw new Error("value must be a JSON string array");
17
+ return parsed as string[];
18
+ }
19
+
20
+ function render(this: DiscussContext, result: unknown): void {
21
+ this.process.stdout.write(this.json ? JSON.stringify(result) : JSON.stringify(result, null, 2));
22
+ }
23
+
24
+ async function call(this: DiscussContext, operation: OperationName, input: Record<string, unknown>): Promise<void> {
25
+ render.call(this, await this.client.call<Record<string, unknown>, unknown>(operation, input));
26
+ }
27
+
28
+ const openCommand = buildCommand({
29
+ func: async function (
30
+ this: DiscussContext,
31
+ flags: {
32
+ title: string;
33
+ actor: string;
34
+ content: string;
35
+ body?: string;
36
+ labelsJson?: string[];
37
+ blocksJson?: string[];
38
+ optionsJson?: string[];
39
+ optionsMode?: string;
40
+ optionDescriptionsJson?: string[];
41
+ },
42
+ ) {
43
+ await call.call(this, "discuss.open", {
44
+ title: flags.title,
45
+ actor: flags.actor,
46
+ content: flags.content,
47
+ body: flags.body,
48
+ labels: flags.labelsJson,
49
+ blocks_task_ids: flags.blocksJson,
50
+ options: flags.optionsJson,
51
+ options_mode: flags.optionsMode,
52
+ option_descriptions: flags.optionDescriptionsJson,
53
+ });
54
+ },
55
+ parameters: {
56
+ flags: {
57
+ title: { brief: "Discussion title", kind: "parsed", parse: String, placeholder: "text" },
58
+ actor: { brief: "Actor opening the discussion", kind: "parsed", parse: String, placeholder: "text" },
59
+ content: { brief: "First round's content", kind: "parsed", parse: String, placeholder: "text" },
60
+ body: { brief: "Longer body text", kind: "parsed", parse: String, placeholder: "text", optional: true },
61
+ labelsJson: { brief: "JSON string array of labels", kind: "parsed", parse: parseStringArray, placeholder: "json", optional: true },
62
+ blocksJson: {
63
+ brief: "JSON string array of task ids to block",
64
+ kind: "parsed",
65
+ parse: parseStringArray,
66
+ placeholder: "json",
67
+ optional: true,
68
+ },
69
+ optionsJson: {
70
+ brief: "JSON string array of posed options",
71
+ kind: "parsed",
72
+ parse: parseStringArray,
73
+ placeholder: "json",
74
+ optional: true,
75
+ },
76
+ optionsMode: { brief: "single or multi", kind: "parsed", parse: String, placeholder: "mode", optional: true },
77
+ optionDescriptionsJson: {
78
+ brief: "JSON string array of option descriptions",
79
+ kind: "parsed",
80
+ parse: parseStringArray,
81
+ placeholder: "json",
82
+ optional: true,
83
+ },
84
+ },
85
+ },
86
+ docs: { brief: "Open a new discussion" },
87
+ });
88
+
89
+ const replyCommand = buildCommand({
90
+ func: async function (
91
+ this: DiscussContext,
92
+ flags: {
93
+ actor: string;
94
+ content: string;
95
+ selectedJson?: string[];
96
+ optionsJson?: string[];
97
+ optionsMode?: string;
98
+ optionDescriptionsJson?: string[];
99
+ },
100
+ id: string,
101
+ ) {
102
+ await call.call(this, "discuss.reply", {
103
+ id,
104
+ actor: flags.actor,
105
+ content: flags.content,
106
+ selected: flags.selectedJson,
107
+ options: flags.optionsJson,
108
+ options_mode: flags.optionsMode,
109
+ option_descriptions: flags.optionDescriptionsJson,
110
+ });
111
+ },
112
+ parameters: {
113
+ flags: {
114
+ actor: { brief: "Actor replying", kind: "parsed", parse: String, placeholder: "text" },
115
+ content: { brief: "Reply content", kind: "parsed", parse: String, placeholder: "text" },
116
+ selectedJson: {
117
+ brief: "JSON string array answering a pending posed choice",
118
+ kind: "parsed",
119
+ parse: parseStringArray,
120
+ placeholder: "json",
121
+ optional: true,
122
+ },
123
+ optionsJson: {
124
+ brief: "JSON string array of posed options",
125
+ kind: "parsed",
126
+ parse: parseStringArray,
127
+ placeholder: "json",
128
+ optional: true,
129
+ },
130
+ optionsMode: { brief: "single or multi", kind: "parsed", parse: String, placeholder: "mode", optional: true },
131
+ optionDescriptionsJson: {
132
+ brief: "JSON string array of option descriptions",
133
+ kind: "parsed",
134
+ parse: parseStringArray,
135
+ placeholder: "json",
136
+ optional: true,
137
+ },
138
+ },
139
+ positional: { kind: "tuple", parameters: [{ brief: "Discussion id", parse: String, placeholder: "id" }] },
140
+ },
141
+ docs: { brief: "Add a round to an existing discussion" },
142
+ });
143
+
144
+ const deferCommand = buildCommand({
145
+ func: async function (this: DiscussContext, flags: { reason?: string }, id: string) {
146
+ await call.call(this, "discuss.defer", { id, reason: flags.reason });
147
+ },
148
+ parameters: {
149
+ flags: {
150
+ reason: { brief: "Why this discussion is being deferred", kind: "parsed", parse: String, placeholder: "text", optional: true },
151
+ },
152
+ positional: { kind: "tuple", parameters: [{ brief: "Discussion id", parse: String, placeholder: "id" }] },
153
+ },
154
+ docs: { brief: "Pause a discussion without settling it" },
155
+ });
156
+
157
+ const resumeCommand = buildCommand({
158
+ func: async function (this: DiscussContext, _flags: Record<string, never>, id: string) {
159
+ await call.call(this, "discuss.resume", { id });
160
+ },
161
+ parameters: { flags: {}, positional: { kind: "tuple", parameters: [{ brief: "Discussion id", parse: String, placeholder: "id" }] } },
162
+ docs: { brief: "Resume a deferred discussion" },
163
+ });
164
+
165
+ const settleCommand = buildCommand({
166
+ func: async function (this: DiscussContext, flags: { settlement: string }, id: string) {
167
+ await call.call(this, "discuss.settle", { id, settlement: flags.settlement });
168
+ },
169
+ parameters: {
170
+ flags: { settlement: { brief: "Final settlement text", kind: "parsed", parse: String, placeholder: "text" } },
171
+ positional: { kind: "tuple", parameters: [{ brief: "Discussion id", parse: String, placeholder: "id" }] },
172
+ },
173
+ docs: { brief: "Settle a discussion" },
174
+ });
175
+
176
+ const blockCommand = buildCommand({
177
+ func: async function (this: DiscussContext, flags: { taskId: string }, id: string) {
178
+ await call.call(this, "discuss.block", { id, task_id: flags.taskId });
179
+ },
180
+ parameters: {
181
+ flags: { taskId: { brief: "Task id to block", kind: "parsed", parse: String, placeholder: "id" } },
182
+ positional: { kind: "tuple", parameters: [{ brief: "Discussion id", parse: String, placeholder: "id" }] },
183
+ },
184
+ docs: { brief: "Block a task's completion on this discussion" },
185
+ });
186
+
187
+ const unblockCommand = buildCommand({
188
+ func: async function (this: DiscussContext, flags: { taskId: string }, id: string) {
189
+ await call.call(this, "discuss.unblock", { id, task_id: flags.taskId });
190
+ },
191
+ parameters: {
192
+ flags: { taskId: { brief: "Task id to unblock", kind: "parsed", parse: String, placeholder: "id" } },
193
+ positional: { kind: "tuple", parameters: [{ brief: "Discussion id", parse: String, placeholder: "id" }] },
194
+ },
195
+ docs: { brief: "Remove a discussion's block on a task" },
196
+ });
197
+
198
+ const showCommand = buildCommand({
199
+ func: async function (this: DiscussContext, _flags: Record<string, never>, id: string) {
200
+ await call.call(this, "discuss.show", { id });
201
+ },
202
+ parameters: { flags: {}, positional: { kind: "tuple", parameters: [{ brief: "Discussion id", parse: String, placeholder: "id" }] } },
203
+ docs: { brief: "Show a discussion's full transcript" },
204
+ });
205
+
206
+ const roundsCommand = buildCommand({
207
+ func: async function (this: DiscussContext, flags: { afterRound?: number; limit?: number }, id: string) {
208
+ await call.call(this, "discuss.rounds", { id, after_round: flags.afterRound, limit: flags.limit });
209
+ },
210
+ parameters: {
211
+ flags: {
212
+ afterRound: { brief: "Only rounds after this number", kind: "parsed", parse: numberParser, optional: true },
213
+ limit: { brief: "Maximum rounds to return", kind: "parsed", parse: numberParser, optional: true },
214
+ },
215
+ positional: { kind: "tuple", parameters: [{ brief: "Discussion id", parse: String, placeholder: "id" }] },
216
+ },
217
+ docs: { brief: "List a discussion's rounds" },
218
+ });
219
+
220
+ const listCommand = buildCommand({
221
+ func: async function (this: DiscussContext, flags: { state?: string; limit?: number }) {
222
+ await call.call(this, "discuss.list", { state: flags.state, limit: flags.limit });
223
+ },
224
+ parameters: {
225
+ flags: {
226
+ state: { brief: "Filter to active|deferred|settled", kind: "parsed", parse: String, placeholder: "state", optional: true },
227
+ limit: { brief: "Maximum discussions to return", kind: "parsed", parse: numberParser, optional: true },
228
+ },
229
+ },
230
+ docs: { brief: "List discussions" },
231
+ });
232
+
233
+ const app = buildApplication(
234
+ buildRouteMap({
235
+ routes: {
236
+ open: openCommand,
237
+ reply: replyCommand,
238
+ defer: deferCommand,
239
+ resume: resumeCommand,
240
+ settle: settleCommand,
241
+ block: blockCommand,
242
+ unblock: unblockCommand,
243
+ show: showCommand,
244
+ rounds: roundsCommand,
245
+ list: listCommand,
246
+ },
247
+ docs: { brief: "Discussion operations" },
248
+ }),
249
+ { name: "discuss", scanner: { caseStyle: "allow-kebab-for-camel" } },
250
+ );
251
+
252
+ export async function runDiscussCli(args: string[], client: DiscussClient): Promise<string> {
253
+ const json = args.includes("--json");
254
+ const positional = args.filter((arg) => arg !== "--json");
255
+ return runStricliToString(app, positional, { client, json });
256
+ }