@hegemonart/get-design-done 1.20.0 → 1.22.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/.claude-plugin/marketplace.json +9 -12
- package/.claude-plugin/plugin.json +8 -31
- package/CHANGELOG.md +200 -0
- package/README.md +48 -7
- package/bin/gdd-sdk +55 -0
- package/hooks/_hook-emit.js +81 -0
- package/hooks/gdd-bash-guard.js +8 -0
- package/hooks/gdd-decision-injector.js +2 -0
- package/hooks/gdd-protected-paths.js +8 -0
- package/hooks/gdd-trajectory-capture.js +64 -0
- package/hooks/hooks.json +9 -0
- package/package.json +19 -47
- package/reference/codex-tools.md +53 -0
- package/reference/gemini-tools.md +53 -0
- package/reference/registry.json +14 -0
- package/scripts/cli/gdd-events.mjs +283 -0
- package/scripts/e2e/run-headless.ts +514 -0
- package/scripts/lib/cli/commands/audit.ts +382 -0
- package/scripts/lib/cli/commands/init.ts +217 -0
- package/scripts/lib/cli/commands/query.ts +329 -0
- package/scripts/lib/cli/commands/run.ts +656 -0
- package/scripts/lib/cli/commands/stage.ts +468 -0
- package/scripts/lib/cli/index.ts +167 -0
- package/scripts/lib/cli/parse-args.ts +336 -0
- package/scripts/lib/connection-probe/index.cjs +263 -0
- package/scripts/lib/context-engine/index.ts +116 -0
- package/scripts/lib/context-engine/manifest.ts +69 -0
- package/scripts/lib/context-engine/truncate.ts +282 -0
- package/scripts/lib/context-engine/types.ts +59 -0
- package/scripts/lib/discuss-parallel-runner/aggregator.ts +448 -0
- package/scripts/lib/discuss-parallel-runner/discussants.ts +430 -0
- package/scripts/lib/discuss-parallel-runner/index.ts +223 -0
- package/scripts/lib/discuss-parallel-runner/types.ts +184 -0
- package/scripts/lib/event-chain.cjs +177 -0
- package/scripts/lib/event-stream/index.ts +31 -1
- package/scripts/lib/event-stream/reader.ts +139 -0
- package/scripts/lib/event-stream/types.ts +155 -1
- package/scripts/lib/event-stream/writer.ts +65 -8
- package/scripts/lib/explore-parallel-runner/index.ts +294 -0
- package/scripts/lib/explore-parallel-runner/mappers.ts +290 -0
- package/scripts/lib/explore-parallel-runner/synthesizer.ts +295 -0
- package/scripts/lib/explore-parallel-runner/types.ts +139 -0
- package/scripts/lib/harness/detect.ts +90 -0
- package/scripts/lib/harness/index.ts +64 -0
- package/scripts/lib/harness/tool-map.ts +142 -0
- package/scripts/lib/init-runner/index.ts +396 -0
- package/scripts/lib/init-runner/researchers.ts +245 -0
- package/scripts/lib/init-runner/scaffold.ts +224 -0
- package/scripts/lib/init-runner/synthesizer.ts +224 -0
- package/scripts/lib/init-runner/types.ts +143 -0
- package/scripts/lib/logger/index.ts +251 -0
- package/scripts/lib/logger/sinks.ts +269 -0
- package/scripts/lib/logger/types.ts +110 -0
- package/scripts/lib/pipeline-runner/human-gate.ts +134 -0
- package/scripts/lib/pipeline-runner/index.ts +527 -0
- package/scripts/lib/pipeline-runner/stage-handlers.ts +339 -0
- package/scripts/lib/pipeline-runner/state-machine.ts +144 -0
- package/scripts/lib/pipeline-runner/types.ts +183 -0
- package/scripts/lib/redact.cjs +122 -0
- package/scripts/lib/session-runner/errors.ts +406 -0
- package/scripts/lib/session-runner/index.ts +715 -0
- package/scripts/lib/session-runner/transcript.ts +189 -0
- package/scripts/lib/session-runner/types.ts +144 -0
- package/scripts/lib/tool-scoping/index.ts +219 -0
- package/scripts/lib/tool-scoping/parse-agent-tools.ts +207 -0
- package/scripts/lib/tool-scoping/stage-scopes.ts +139 -0
- package/scripts/lib/tool-scoping/types.ts +77 -0
- package/scripts/lib/trajectory/index.cjs +126 -0
- package/scripts/lib/transports/ws.cjs +179 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* gdd-trajectory-capture.js — PostToolUse:Agent hook (Plan 22-03).
|
|
4
|
+
*
|
|
5
|
+
* Reads the standard Claude Code hook JSON from stdin:
|
|
6
|
+
* { tool_name, tool_input, tool_response, session_id, ... }
|
|
7
|
+
*
|
|
8
|
+
* Writes one JSONL line to `.design/telemetry/trajectories/<cycle>.jsonl`
|
|
9
|
+
* via `scripts/lib/trajectory/index.cjs`. Silent-on-failure: telemetry
|
|
10
|
+
* never blocks the parent agent's tool call.
|
|
11
|
+
*
|
|
12
|
+
* Cycle resolution:
|
|
13
|
+
* * env var GDD_CYCLE wins (used by Phase 21 pipeline runner)
|
|
14
|
+
* * fallback: 'current'
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
'use strict';
|
|
18
|
+
|
|
19
|
+
const { recordCall } = require('../scripts/lib/trajectory/index.cjs');
|
|
20
|
+
|
|
21
|
+
let raw = '';
|
|
22
|
+
process.stdin.setEncoding('utf8');
|
|
23
|
+
process.stdin.on('data', (chunk) => {
|
|
24
|
+
raw += chunk;
|
|
25
|
+
});
|
|
26
|
+
process.stdin.on('end', () => {
|
|
27
|
+
try {
|
|
28
|
+
const input = raw.trim() ? JSON.parse(raw) : {};
|
|
29
|
+
const toolName = input.tool_name || input.toolName || 'unknown';
|
|
30
|
+
const toolInput = input.tool_input ?? input.toolInput ?? null;
|
|
31
|
+
const toolResponse = input.tool_response ?? input.toolResponse ?? null;
|
|
32
|
+
const sessionId = input.session_id ?? input.sessionId ?? null;
|
|
33
|
+
const status =
|
|
34
|
+
toolResponse && typeof toolResponse === 'object' && toolResponse.is_error === true
|
|
35
|
+
? 'error'
|
|
36
|
+
: 'ok';
|
|
37
|
+
const latency = typeof input.latency_ms === 'number' ? input.latency_ms : 0;
|
|
38
|
+
|
|
39
|
+
recordCall({
|
|
40
|
+
cycle: process.env.GDD_CYCLE || 'current',
|
|
41
|
+
session_id: sessionId,
|
|
42
|
+
agent: input.agent || process.env.GDD_AGENT || 'unknown',
|
|
43
|
+
tool: toolName,
|
|
44
|
+
args: toolInput,
|
|
45
|
+
result: toolResponse,
|
|
46
|
+
latency_ms: latency,
|
|
47
|
+
status,
|
|
48
|
+
});
|
|
49
|
+
} catch (err) {
|
|
50
|
+
try {
|
|
51
|
+
process.stderr.write(
|
|
52
|
+
`[gdd-trajectory] hook failed: ${err && err.message ? err.message : String(err)}\n`,
|
|
53
|
+
);
|
|
54
|
+
} catch {
|
|
55
|
+
/* swallow */
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// Always emit a non-blocking continue verdict.
|
|
59
|
+
try {
|
|
60
|
+
process.stdout.write(JSON.stringify({ continue: true, suppressOutput: true }));
|
|
61
|
+
} catch {
|
|
62
|
+
/* swallow */
|
|
63
|
+
}
|
|
64
|
+
});
|
package/hooks/hooks.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hegemonart/get-design-done",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.22.0",
|
|
4
4
|
"description": "A Claude Code plugin for systematic design improvement",
|
|
5
5
|
"author": "Hegemon",
|
|
6
6
|
"homepage": "https://github.com/hegemonart/get-design-done",
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"scripts/",
|
|
21
21
|
"connections/",
|
|
22
22
|
"reference/",
|
|
23
|
+
"bin/",
|
|
23
24
|
"SKILL.md",
|
|
24
25
|
"README.md",
|
|
25
26
|
"CHANGELOG.md",
|
|
@@ -27,7 +28,9 @@
|
|
|
27
28
|
],
|
|
28
29
|
"bin": {
|
|
29
30
|
"get-design-done": "./scripts/install.cjs",
|
|
30
|
-
"gdd-state-mcp": "./scripts/mcp-servers/gdd-state/server.ts"
|
|
31
|
+
"gdd-state-mcp": "./scripts/mcp-servers/gdd-state/server.ts",
|
|
32
|
+
"gdd-sdk": "./bin/gdd-sdk",
|
|
33
|
+
"gdd-events": "./scripts/cli/gdd-events.mjs"
|
|
31
34
|
},
|
|
32
35
|
"publishConfig": {
|
|
33
36
|
"access": "public",
|
|
@@ -45,7 +48,9 @@
|
|
|
45
48
|
"scan:injection": "node scripts/run-injection-scanner-ci.cjs",
|
|
46
49
|
"test:size-budget": "node --test tests/agent-size-budget.test.cjs",
|
|
47
50
|
"release:extract-changelog": "node scripts/extract-changelog-section.cjs",
|
|
48
|
-
"verify:version-sync": "node scripts/verify-version-sync.cjs"
|
|
51
|
+
"verify:version-sync": "node scripts/verify-version-sync.cjs",
|
|
52
|
+
"typecheck:session-runner": "tsc --noEmit",
|
|
53
|
+
"gdd-sdk": "node --experimental-strip-types scripts/lib/cli/index.ts"
|
|
49
54
|
},
|
|
50
55
|
"devDependencies": {
|
|
51
56
|
"@types/node": "^22.0.0",
|
|
@@ -65,56 +70,23 @@
|
|
|
65
70
|
"reflection",
|
|
66
71
|
"tested",
|
|
67
72
|
"ci",
|
|
68
|
-
"
|
|
69
|
-
"
|
|
70
|
-
"
|
|
71
|
-
"
|
|
72
|
-
"
|
|
73
|
-
"
|
|
74
|
-
"
|
|
75
|
-
"make-interfaces-feel-better",
|
|
76
|
-
"palette-catalog",
|
|
77
|
-
"style-vocabulary",
|
|
78
|
-
"industry-palettes",
|
|
79
|
-
"ui-style-vocabulary",
|
|
80
|
-
"variable-fonts",
|
|
81
|
-
"container-queries",
|
|
82
|
-
"view-transitions",
|
|
83
|
-
"motion-vocabulary",
|
|
84
|
-
"motion-easings",
|
|
85
|
-
"transition-taxonomy",
|
|
86
|
-
"gesture-mechanics",
|
|
87
|
-
"clip-path-animation",
|
|
88
|
-
"component-specs",
|
|
89
|
-
"design-system-benchmarks",
|
|
90
|
-
"i18n",
|
|
91
|
-
"user-research",
|
|
92
|
-
"information-architecture",
|
|
93
|
-
"form-patterns",
|
|
94
|
-
"data-viz",
|
|
95
|
-
"platforms",
|
|
96
|
-
"cross-cycle-memory",
|
|
97
|
-
"fts5",
|
|
98
|
-
"checkpoints",
|
|
99
|
-
"experience-archive",
|
|
100
|
-
"recall",
|
|
101
|
-
"relevance-counter",
|
|
102
|
-
"record-contract",
|
|
103
|
-
"first-principles",
|
|
104
|
-
"emotional-design",
|
|
105
|
-
"component-authoring",
|
|
106
|
-
"disney-12-principles",
|
|
107
|
-
"peak-end-rule",
|
|
108
|
-
"loss-aversion",
|
|
109
|
-
"cognitive-load",
|
|
110
|
-
"doherty-threshold",
|
|
111
|
-
"rams-lens"
|
|
73
|
+
"headless",
|
|
74
|
+
"cli",
|
|
75
|
+
"codex",
|
|
76
|
+
"gemini",
|
|
77
|
+
"mcp",
|
|
78
|
+
"parallel-agents",
|
|
79
|
+
"agent-sdk"
|
|
112
80
|
],
|
|
113
81
|
"skills": [
|
|
114
82
|
"SKILL.md"
|
|
115
83
|
],
|
|
116
84
|
"hooks": "hooks/hooks.json",
|
|
117
85
|
"dependencies": {
|
|
86
|
+
"@anthropic-ai/claude-agent-sdk": "^0.2.119",
|
|
118
87
|
"@modelcontextprotocol/sdk": "^1.0.0"
|
|
88
|
+
},
|
|
89
|
+
"optionalDependencies": {
|
|
90
|
+
"ws": "^8.20.0"
|
|
119
91
|
}
|
|
120
92
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Codex CLI Tool Map
|
|
2
|
+
|
|
3
|
+
Last verified: 2026-04-24
|
|
4
|
+
|
|
5
|
+
When a GDD skill references a Claude Code tool name, the Codex runtime
|
|
6
|
+
translates to the equivalent below. Skills do NOT need to branch — the tool
|
|
7
|
+
name in prose is authoritative; Codex resolves via this map.
|
|
8
|
+
|
|
9
|
+
## Tool-name mapping
|
|
10
|
+
|
|
11
|
+
| CC name | Codex name | Notes |
|
|
12
|
+
| --- | --- | --- |
|
|
13
|
+
| `Read` | `read_file` | Takes `path`; returns file content. |
|
|
14
|
+
| `Write` | `apply_patch` (create mode) | Requires a diff-style patch; ensure tool call emits `{action:'create', path, content}`. |
|
|
15
|
+
| `Edit` | `apply_patch` (update mode) | Emits `{action:'update', path, patch}` with unified diff. |
|
|
16
|
+
| `Bash` | `shell` | Takes `{command: string, cwd?, timeout_sec?}`. |
|
|
17
|
+
| `Grep` | `shell` | Compose a `rg` / `grep -rn` invocation; no native Codex grep tool. |
|
|
18
|
+
| `Glob` | `shell` | Compose `ls` / `find`; no native Codex glob. |
|
|
19
|
+
| `Task` | Sub-invocation via nested Codex | Codex spawns nested sessions via its own CLI, not a tool call. Skills requiring Task should prefer the MCP `gdd-state` tool layer instead. |
|
|
20
|
+
| `WebSearch` | `web_search` | If enabled in Codex policy. |
|
|
21
|
+
| `WebFetch` | `shell` (curl) or `web_search.open` | Prefer curl for deterministic output. |
|
|
22
|
+
|
|
23
|
+
## MCP server `gdd-state`
|
|
24
|
+
|
|
25
|
+
The gdd-state MCP server works unchanged on Codex. Configure Codex to load
|
|
26
|
+
it by adding to `~/.codex/config.toml`:
|
|
27
|
+
|
|
28
|
+
```toml
|
|
29
|
+
[[mcp_servers]]
|
|
30
|
+
name = "gdd-state"
|
|
31
|
+
command = "node"
|
|
32
|
+
args = ["--experimental-strip-types", "<pkg-root>/scripts/mcp-servers/gdd-state/server.ts"]
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
All 11 tools exposed by the server appear as `mcp__gdd_state__*` in Codex.
|
|
36
|
+
|
|
37
|
+
## Known gaps
|
|
38
|
+
|
|
39
|
+
- `Task` spawning: Codex does not expose nested-session as a tool call. For
|
|
40
|
+
now, skills that rely on `Task` (parallel mappers in Plan 21-06, parallel
|
|
41
|
+
discussants in Plan 21-07) should invoke the gdd-sdk CLI as a shell
|
|
42
|
+
subprocess: `shell("npx gdd-sdk stage explore --parallel")`. This is
|
|
43
|
+
documented in AGENTS.md.
|
|
44
|
+
- `apply_patch` diff format differs from CC's Edit: Codex expects unified
|
|
45
|
+
diff (`---`/`+++`/`@@` hunks), while CC's Edit takes `old_string`/`new_string`.
|
|
46
|
+
The plugin's skill prose uses Edit's semantics; on Codex the runtime must
|
|
47
|
+
generate the unified diff from the old/new pair. A helper lives at
|
|
48
|
+
`scripts/lib/harness/edit-to-patch.ts` (reserved for Phase 22 wiring).
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
Last verified: 2026-04-24 — tool surface re-checked against Codex CLI docs
|
|
53
|
+
current to this date. Revisit whenever Codex ships a tool-vocabulary change.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Gemini CLI Tool Map
|
|
2
|
+
|
|
3
|
+
Last verified: 2026-04-24
|
|
4
|
+
|
|
5
|
+
When a GDD skill references a Claude Code tool name, the Gemini runtime
|
|
6
|
+
translates to the equivalent below. Skills do NOT need to branch — the tool
|
|
7
|
+
name in prose is authoritative; Gemini resolves via this map.
|
|
8
|
+
|
|
9
|
+
## Tool-name mapping
|
|
10
|
+
|
|
11
|
+
| CC name | Gemini name | Notes |
|
|
12
|
+
| --- | --- | --- |
|
|
13
|
+
| `Read` | `read_file` | Takes `absolute_path`. |
|
|
14
|
+
| `Write` | `write_file` | Takes `path`, `content`; overwrites. |
|
|
15
|
+
| `Edit` | `replace` | Takes `file_path`, `old_string`, `new_string`. Semantics match CC. |
|
|
16
|
+
| `Bash` | `run_shell_command` | Takes `{command: string, directory?}`. |
|
|
17
|
+
| `Grep` | `search_file_content` | Native grep wrapper. |
|
|
18
|
+
| `Glob` | `glob` | Native glob wrapper. |
|
|
19
|
+
| `Task` | Sub-invocation via nested gemini CLI | Same gap as Codex. |
|
|
20
|
+
| `WebSearch` | `google_web_search` | Built-in. |
|
|
21
|
+
| `WebFetch` | `web_fetch` | Built-in. |
|
|
22
|
+
|
|
23
|
+
## MCP server `gdd-state`
|
|
24
|
+
|
|
25
|
+
The gdd-state MCP server works unchanged on Gemini. Configure Gemini to load
|
|
26
|
+
it by adding to `~/.gemini/settings.json`:
|
|
27
|
+
|
|
28
|
+
```json
|
|
29
|
+
{
|
|
30
|
+
"mcpServers": {
|
|
31
|
+
"gdd-state": {
|
|
32
|
+
"command": "node",
|
|
33
|
+
"args": ["--experimental-strip-types", "<pkg-root>/scripts/mcp-servers/gdd-state/server.ts"]
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
All 11 tools exposed by the server appear as `mcp__gdd_state__*` in Gemini.
|
|
40
|
+
|
|
41
|
+
## Known gaps
|
|
42
|
+
|
|
43
|
+
- `Task` spawning: same as Codex — prefer `run_shell_command("npx gdd-sdk stage ...")`.
|
|
44
|
+
See GEMINI.md for invocation details.
|
|
45
|
+
- Gemini's `replace` has stricter uniqueness requirements than CC's Edit;
|
|
46
|
+
when `old_string` appears more than once, Gemini requires context lines.
|
|
47
|
+
Skill prose that calls Edit should include surrounding context in
|
|
48
|
+
`old_string` to satisfy both harnesses.
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
Last verified: 2026-04-24 — tool surface re-checked against Gemini CLI docs
|
|
53
|
+
current to this date. Revisit whenever Gemini ships a tool-vocabulary change.
|
package/reference/registry.json
CHANGED
|
@@ -3,6 +3,20 @@
|
|
|
3
3
|
"version": 1,
|
|
4
4
|
"generated_at": "2026-04-24T00:00:00.000Z",
|
|
5
5
|
"entries": [
|
|
6
|
+
{
|
|
7
|
+
"name": "codex-tools",
|
|
8
|
+
"path": "reference/codex-tools.md",
|
|
9
|
+
"type": "meta-rules",
|
|
10
|
+
"phase": 21,
|
|
11
|
+
"description": "Phase 21 cross-harness tool map — Claude Code → OpenAI Codex CLI tool-name equivalents for the gdd-sdk runner (Read→read_file, Write/Edit→apply_patch, Bash→shell, etc.)"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"name": "gemini-tools",
|
|
15
|
+
"path": "reference/gemini-tools.md",
|
|
16
|
+
"type": "meta-rules",
|
|
17
|
+
"phase": 21,
|
|
18
|
+
"description": "Phase 21 cross-harness tool map — Claude Code → Gemini CLI tool-name equivalents for the gdd-sdk runner"
|
|
19
|
+
},
|
|
6
20
|
{
|
|
7
21
|
"name": "error-recovery",
|
|
8
22
|
"path": "reference/error-recovery.md",
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
#!/usr/bin/env -S node --experimental-strip-types
|
|
2
|
+
// scripts/cli/gdd-events.mjs — CLI transport for the event stream
|
|
3
|
+
// (Plan 22-06).
|
|
4
|
+
//
|
|
5
|
+
// Subcommands:
|
|
6
|
+
// gdd-events tail [--follow] [--path=<p>]
|
|
7
|
+
// - dump events.jsonl to stdout, line-by-line
|
|
8
|
+
// - --follow re-polls every 250ms appending new content (no native
|
|
9
|
+
// inotify dep; portable across platforms)
|
|
10
|
+
//
|
|
11
|
+
// gdd-events grep <filter> [--path=<p>]
|
|
12
|
+
// - filter language (space-separated terms, all AND'd):
|
|
13
|
+
// type=<exact-string> — match `type` field
|
|
14
|
+
// payload.<dotted.path>=<value> — drill into payload by '.'-path
|
|
15
|
+
// !type=<exact-string> — negate
|
|
16
|
+
// !payload.<path>=<value> — negate
|
|
17
|
+
// - prints matching events to stdout as JSONL (compact)
|
|
18
|
+
//
|
|
19
|
+
// gdd-events cat [--path=<p>]
|
|
20
|
+
// - alias for tail without --follow, but pretty-prints with a
|
|
21
|
+
// leading timestamp+type prefix per line
|
|
22
|
+
//
|
|
23
|
+
// gdd-events list-types
|
|
24
|
+
// - prints the runtime KNOWN_EVENT_TYPES list (from Plan 22-01)
|
|
25
|
+
//
|
|
26
|
+
// gdd-events serve [--port=<n>] [--token=<t>] [--tail=<file>]
|
|
27
|
+
// - WebSocket transport (Plan 22-07). Loaded lazily via
|
|
28
|
+
// probe-optional; helpful error if `ws` is not installed.
|
|
29
|
+
//
|
|
30
|
+
// Default --path is `.design/telemetry/events.jsonl` (relative to cwd).
|
|
31
|
+
|
|
32
|
+
import { existsSync, statSync, openSync, readSync, closeSync } from 'node:fs';
|
|
33
|
+
import { resolve, isAbsolute } from 'node:path';
|
|
34
|
+
import { pathToFileURL } from 'node:url';
|
|
35
|
+
import { argv, exit, stdout, stderr } from 'node:process';
|
|
36
|
+
import { createRequire } from 'node:module';
|
|
37
|
+
|
|
38
|
+
const require = createRequire(import.meta.url);
|
|
39
|
+
|
|
40
|
+
const DEFAULT_PATH = '.design/telemetry/events.jsonl';
|
|
41
|
+
|
|
42
|
+
function usage() {
|
|
43
|
+
stderr.write(
|
|
44
|
+
[
|
|
45
|
+
'gdd-events — Phase 22 event-stream CLI',
|
|
46
|
+
'',
|
|
47
|
+
'Usage:',
|
|
48
|
+
' gdd-events tail [--follow] [--path=<p>]',
|
|
49
|
+
' gdd-events grep <filter…> [--path=<p>]',
|
|
50
|
+
' gdd-events cat [--path=<p>]',
|
|
51
|
+
' gdd-events list-types',
|
|
52
|
+
' gdd-events serve [--port=<n>] [--token=<t>] [--tail=<file>]',
|
|
53
|
+
'',
|
|
54
|
+
'Filter language (grep): type=<s> payload.<dotted.path>=<s> !type=<s>',
|
|
55
|
+
'',
|
|
56
|
+
].join('\n'),
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function parseArgs(args) {
|
|
61
|
+
const out = { _: [], flags: {} };
|
|
62
|
+
for (const a of args) {
|
|
63
|
+
if (a.startsWith('--')) {
|
|
64
|
+
const eq = a.indexOf('=');
|
|
65
|
+
if (eq === -1) {
|
|
66
|
+
out.flags[a.slice(2)] = true;
|
|
67
|
+
} else {
|
|
68
|
+
out.flags[a.slice(2, eq)] = a.slice(eq + 1);
|
|
69
|
+
}
|
|
70
|
+
} else {
|
|
71
|
+
out._.push(a);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function resolvePath(flagPath) {
|
|
78
|
+
const raw = flagPath || DEFAULT_PATH;
|
|
79
|
+
return isAbsolute(raw) ? raw : resolve(process.cwd(), raw);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Compile filter terms like "type=foo", "!payload.x=1" into a predicate. */
|
|
83
|
+
export function compileFilter(terms) {
|
|
84
|
+
/** @type {Array<(ev: any) => boolean>} */
|
|
85
|
+
const checks = [];
|
|
86
|
+
for (const term of terms) {
|
|
87
|
+
let negate = false;
|
|
88
|
+
let body = term;
|
|
89
|
+
if (body.startsWith('!')) {
|
|
90
|
+
negate = true;
|
|
91
|
+
body = body.slice(1);
|
|
92
|
+
}
|
|
93
|
+
const eq = body.indexOf('=');
|
|
94
|
+
if (eq === -1) {
|
|
95
|
+
throw new Error(`gdd-events: bad filter term: ${term}`);
|
|
96
|
+
}
|
|
97
|
+
const key = body.slice(0, eq);
|
|
98
|
+
const want = body.slice(eq + 1);
|
|
99
|
+
/** @type {(ev: any) => boolean} */
|
|
100
|
+
let test;
|
|
101
|
+
if (key === 'type') {
|
|
102
|
+
test = (ev) => ev?.type === want;
|
|
103
|
+
} else if (key.startsWith('payload.')) {
|
|
104
|
+
const path = key.slice('payload.'.length).split('.');
|
|
105
|
+
test = (ev) => {
|
|
106
|
+
let cur = ev?.payload;
|
|
107
|
+
for (const part of path) {
|
|
108
|
+
if (cur == null || typeof cur !== 'object') return false;
|
|
109
|
+
cur = cur[part];
|
|
110
|
+
}
|
|
111
|
+
return String(cur) === want;
|
|
112
|
+
};
|
|
113
|
+
} else if (key === 'stage') {
|
|
114
|
+
test = (ev) => ev?.stage === want;
|
|
115
|
+
} else if (key === 'cycle') {
|
|
116
|
+
test = (ev) => ev?.cycle === want;
|
|
117
|
+
} else if (key === 'sessionId') {
|
|
118
|
+
test = (ev) => ev?.sessionId === want;
|
|
119
|
+
} else {
|
|
120
|
+
throw new Error(`gdd-events: unsupported filter key: ${key}`);
|
|
121
|
+
}
|
|
122
|
+
checks.push(negate ? (ev) => !test(ev) : test);
|
|
123
|
+
}
|
|
124
|
+
return (ev) => checks.every((c) => c(ev));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function cmdTail(parsed) {
|
|
128
|
+
const path = resolvePath(parsed.flags.path);
|
|
129
|
+
const { readEvents } = await import('../lib/event-stream/reader.ts');
|
|
130
|
+
if (!parsed.flags.follow) {
|
|
131
|
+
for await (const ev of readEvents({ path })) {
|
|
132
|
+
stdout.write(JSON.stringify(ev) + '\n');
|
|
133
|
+
}
|
|
134
|
+
return 0;
|
|
135
|
+
}
|
|
136
|
+
// Follow mode: stream existing content, then poll for appends.
|
|
137
|
+
let offset = 0;
|
|
138
|
+
if (existsSync(path)) {
|
|
139
|
+
for await (const ev of readEvents({ path })) {
|
|
140
|
+
stdout.write(JSON.stringify(ev) + '\n');
|
|
141
|
+
}
|
|
142
|
+
offset = statSync(path).size;
|
|
143
|
+
}
|
|
144
|
+
// Poll loop. Reads new bytes since last offset, splits on \n, writes each.
|
|
145
|
+
let buf = '';
|
|
146
|
+
// eslint-disable-next-line no-constant-condition
|
|
147
|
+
while (true) {
|
|
148
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
149
|
+
if (!existsSync(path)) continue;
|
|
150
|
+
const size = statSync(path).size;
|
|
151
|
+
if (size <= offset) continue;
|
|
152
|
+
const fd = openSync(path, 'r');
|
|
153
|
+
try {
|
|
154
|
+
const need = size - offset;
|
|
155
|
+
const chunk = Buffer.allocUnsafe(need);
|
|
156
|
+
const n = readSync(fd, chunk, 0, need, offset);
|
|
157
|
+
offset += n;
|
|
158
|
+
buf += chunk.subarray(0, n).toString('utf8');
|
|
159
|
+
const lines = buf.split('\n');
|
|
160
|
+
buf = lines.pop() || '';
|
|
161
|
+
for (const line of lines) {
|
|
162
|
+
if (line.trim() === '') continue;
|
|
163
|
+
stdout.write(line + '\n');
|
|
164
|
+
}
|
|
165
|
+
} finally {
|
|
166
|
+
closeSync(fd);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function cmdGrep(parsed) {
|
|
172
|
+
const path = resolvePath(parsed.flags.path);
|
|
173
|
+
const terms = parsed._;
|
|
174
|
+
if (terms.length === 0) {
|
|
175
|
+
stderr.write('gdd-events grep: at least one filter term required\n');
|
|
176
|
+
return 2;
|
|
177
|
+
}
|
|
178
|
+
const predicate = compileFilter(terms);
|
|
179
|
+
const { readEvents } = await import('../lib/event-stream/reader.ts');
|
|
180
|
+
for await (const ev of readEvents({ path, predicate })) {
|
|
181
|
+
stdout.write(JSON.stringify(ev) + '\n');
|
|
182
|
+
}
|
|
183
|
+
return 0;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function cmdCat(parsed) {
|
|
187
|
+
const path = resolvePath(parsed.flags.path);
|
|
188
|
+
const { readEvents } = await import('../lib/event-stream/reader.ts');
|
|
189
|
+
for await (const ev of readEvents({ path })) {
|
|
190
|
+
const ts = ev.timestamp ?? '?';
|
|
191
|
+
const tp = ev.type ?? '?';
|
|
192
|
+
stdout.write(`${ts} ${tp.padEnd(28)} ${JSON.stringify(ev.payload ?? {})}\n`);
|
|
193
|
+
}
|
|
194
|
+
return 0;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async function cmdListTypes() {
|
|
198
|
+
const { KNOWN_EVENT_TYPES } = await import('../lib/event-stream/types.ts');
|
|
199
|
+
for (const t of KNOWN_EVENT_TYPES) stdout.write(t + '\n');
|
|
200
|
+
return 0;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async function cmdServe(parsed) {
|
|
204
|
+
let mod;
|
|
205
|
+
try {
|
|
206
|
+
mod = require('../lib/transports/ws.cjs');
|
|
207
|
+
} catch (err) {
|
|
208
|
+
stderr.write(
|
|
209
|
+
'gdd-events serve: WebSocket transport requires the optional `ws` package.\n' +
|
|
210
|
+
' install: npm i -D ws\n' +
|
|
211
|
+
` ${err && err.message ? err.message : String(err)}\n`,
|
|
212
|
+
);
|
|
213
|
+
return 1;
|
|
214
|
+
}
|
|
215
|
+
const port = Number(parsed.flags.port) || 9595;
|
|
216
|
+
const token = parsed.flags.token || process.env.GDD_EVENTS_TOKEN;
|
|
217
|
+
if (!token) {
|
|
218
|
+
stderr.write('gdd-events serve: --token=<t> or GDD_EVENTS_TOKEN env required\n');
|
|
219
|
+
return 2;
|
|
220
|
+
}
|
|
221
|
+
const tailFrom = parsed.flags.tail
|
|
222
|
+
? resolvePath(parsed.flags.tail)
|
|
223
|
+
: resolvePath(undefined);
|
|
224
|
+
// Bridge live bus → ws transport. The transport is CommonJS and cannot
|
|
225
|
+
// require .ts directly, so we import the bus here and pass subscribeAll
|
|
226
|
+
// as a callback factory.
|
|
227
|
+
const { subscribeAll } = await import('../lib/event-stream/index.ts');
|
|
228
|
+
const subscribe = (handler) => subscribeAll(handler);
|
|
229
|
+
const handle = await mod.startServer({ port, token, tailFrom, subscribe });
|
|
230
|
+
stderr.write(`gdd-events: WebSocket listening on :${port} (auth required)\n`);
|
|
231
|
+
// Keep the process alive until SIGINT/SIGTERM.
|
|
232
|
+
await new Promise((resolve) => {
|
|
233
|
+
const close = () => {
|
|
234
|
+
handle.close();
|
|
235
|
+
resolve();
|
|
236
|
+
};
|
|
237
|
+
process.once('SIGINT', close);
|
|
238
|
+
process.once('SIGTERM', close);
|
|
239
|
+
});
|
|
240
|
+
return 0;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async function main() {
|
|
244
|
+
const parsed = parseArgs(argv.slice(2));
|
|
245
|
+
const sub = parsed._.shift();
|
|
246
|
+
try {
|
|
247
|
+
switch (sub) {
|
|
248
|
+
case 'tail':
|
|
249
|
+
return await cmdTail(parsed);
|
|
250
|
+
case 'grep':
|
|
251
|
+
return await cmdGrep(parsed);
|
|
252
|
+
case 'cat':
|
|
253
|
+
return await cmdCat(parsed);
|
|
254
|
+
case 'list-types':
|
|
255
|
+
return await cmdListTypes();
|
|
256
|
+
case 'serve':
|
|
257
|
+
return await cmdServe(parsed);
|
|
258
|
+
case '-h':
|
|
259
|
+
case '--help':
|
|
260
|
+
case 'help':
|
|
261
|
+
usage();
|
|
262
|
+
return 0;
|
|
263
|
+
default:
|
|
264
|
+
usage();
|
|
265
|
+
return sub === undefined ? 0 : 2;
|
|
266
|
+
}
|
|
267
|
+
} catch (err) {
|
|
268
|
+
stderr.write(`gdd-events: ${err && err.message ? err.message : String(err)}\n`);
|
|
269
|
+
return 1;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Compare module URL via pathToFileURL — Windows paths use backslashes
|
|
274
|
+
// and need proper file:// URL canonicalisation; the simpler `file://${argv[1]}`
|
|
275
|
+
// form drops to false on Windows and the CLI silently no-ops.
|
|
276
|
+
const isCli = process.argv[1] !== undefined &&
|
|
277
|
+
import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
278
|
+
if (isCli) {
|
|
279
|
+
main().then((code) => exit(code), (err) => {
|
|
280
|
+
stderr.write(`gdd-events fatal: ${err}\n`);
|
|
281
|
+
exit(1);
|
|
282
|
+
});
|
|
283
|
+
}
|