@united-workforce/cli 0.1.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/LICENSE +21 -0
- package/README.md +221 -0
- package/dist/__tests__/adapter-json-roundtrip.test.d.ts +2 -0
- package/dist/__tests__/adapter-json-roundtrip.test.d.ts.map +1 -0
- package/dist/__tests__/adapter-json-roundtrip.test.js +147 -0
- package/dist/__tests__/adapter-json-roundtrip.test.js.map +1 -0
- package/dist/__tests__/config.test.d.ts +2 -0
- package/dist/__tests__/config.test.d.ts.map +1 -0
- package/dist/__tests__/config.test.js +685 -0
- package/dist/__tests__/config.test.js.map +1 -0
- package/dist/__tests__/current-role.test.d.ts +2 -0
- package/dist/__tests__/current-role.test.d.ts.map +1 -0
- package/dist/__tests__/current-role.test.js +401 -0
- package/dist/__tests__/current-role.test.js.map +1 -0
- package/dist/__tests__/e2e-mock-agent.test.d.ts +2 -0
- package/dist/__tests__/e2e-mock-agent.test.d.ts.map +1 -0
- package/dist/__tests__/e2e-mock-agent.test.js +401 -0
- package/dist/__tests__/e2e-mock-agent.test.js.map +1 -0
- package/dist/__tests__/include-tag.test.d.ts +2 -0
- package/dist/__tests__/include-tag.test.d.ts.map +1 -0
- package/dist/__tests__/include-tag.test.js +69 -0
- package/dist/__tests__/include-tag.test.js.map +1 -0
- package/dist/__tests__/log.test.d.ts +2 -0
- package/dist/__tests__/log.test.d.ts.map +1 -0
- package/dist/__tests__/log.test.js +161 -0
- package/dist/__tests__/log.test.js.map +1 -0
- package/dist/__tests__/moderator-evaluate.test.d.ts +2 -0
- package/dist/__tests__/moderator-evaluate.test.d.ts.map +1 -0
- package/dist/__tests__/moderator-evaluate.test.js +170 -0
- package/dist/__tests__/moderator-evaluate.test.js.map +1 -0
- package/dist/__tests__/preload.d.ts +3 -0
- package/dist/__tests__/preload.d.ts.map +1 -0
- package/dist/__tests__/preload.js +6 -0
- package/dist/__tests__/preload.js.map +1 -0
- package/dist/__tests__/prompt.test.d.ts +2 -0
- package/dist/__tests__/prompt.test.d.ts.map +1 -0
- package/dist/__tests__/prompt.test.js +111 -0
- package/dist/__tests__/prompt.test.js.map +1 -0
- package/dist/__tests__/resolve-head-hash.test.d.ts +2 -0
- package/dist/__tests__/resolve-head-hash.test.d.ts.map +1 -0
- package/dist/__tests__/resolve-head-hash.test.js +66 -0
- package/dist/__tests__/resolve-head-hash.test.js.map +1 -0
- package/dist/__tests__/setup-agent-discovery.test.d.ts +2 -0
- package/dist/__tests__/setup-agent-discovery.test.d.ts.map +1 -0
- package/dist/__tests__/setup-agent-discovery.test.js +119 -0
- package/dist/__tests__/setup-agent-discovery.test.js.map +1 -0
- package/dist/__tests__/setup-complexity.test.d.ts +2 -0
- package/dist/__tests__/setup-complexity.test.d.ts.map +1 -0
- package/dist/__tests__/setup-complexity.test.js +314 -0
- package/dist/__tests__/setup-complexity.test.js.map +1 -0
- package/dist/__tests__/setup-validate.test.d.ts +2 -0
- package/dist/__tests__/setup-validate.test.d.ts.map +1 -0
- package/dist/__tests__/setup-validate.test.js +108 -0
- package/dist/__tests__/setup-validate.test.js.map +1 -0
- package/dist/__tests__/solve-issue-tea-worktree.test.d.ts +2 -0
- package/dist/__tests__/solve-issue-tea-worktree.test.d.ts.map +1 -0
- package/dist/__tests__/solve-issue-tea-worktree.test.js +107 -0
- package/dist/__tests__/solve-issue-tea-worktree.test.js.map +1 -0
- package/dist/__tests__/spawn-agent-json.test.d.ts +2 -0
- package/dist/__tests__/spawn-agent-json.test.d.ts.map +1 -0
- package/dist/__tests__/spawn-agent-json.test.js +79 -0
- package/dist/__tests__/spawn-agent-json.test.js.map +1 -0
- package/dist/__tests__/step-read.test.d.ts +2 -0
- package/dist/__tests__/step-read.test.d.ts.map +1 -0
- package/dist/__tests__/step-read.test.js +561 -0
- package/dist/__tests__/step-read.test.js.map +1 -0
- package/dist/__tests__/step-show-json.test.d.ts +2 -0
- package/dist/__tests__/step-show-json.test.d.ts.map +1 -0
- package/dist/__tests__/step-show-json.test.js +311 -0
- package/dist/__tests__/step-show-json.test.js.map +1 -0
- package/dist/__tests__/step-timing.test.d.ts +2 -0
- package/dist/__tests__/step-timing.test.d.ts.map +1 -0
- package/dist/__tests__/step-timing.test.js +345 -0
- package/dist/__tests__/step-timing.test.js.map +1 -0
- package/dist/__tests__/store-global-cas.test.d.ts +2 -0
- package/dist/__tests__/store-global-cas.test.d.ts.map +1 -0
- package/dist/__tests__/store-global-cas.test.js +235 -0
- package/dist/__tests__/store-global-cas.test.js.map +1 -0
- package/dist/__tests__/store-storage-root.test.d.ts +2 -0
- package/dist/__tests__/store-storage-root.test.d.ts.map +1 -0
- package/dist/__tests__/store-storage-root.test.js +43 -0
- package/dist/__tests__/store-storage-root.test.js.map +1 -0
- package/dist/__tests__/store-unified-threads.test.d.ts +2 -0
- package/dist/__tests__/store-unified-threads.test.d.ts.map +1 -0
- package/dist/__tests__/store-unified-threads.test.js +189 -0
- package/dist/__tests__/store-unified-threads.test.js.map +1 -0
- package/dist/__tests__/thread-cancel-status.test.d.ts +2 -0
- package/dist/__tests__/thread-cancel-status.test.d.ts.map +1 -0
- package/dist/__tests__/thread-cancel-status.test.js +111 -0
- package/dist/__tests__/thread-cancel-status.test.js.map +1 -0
- package/dist/__tests__/thread-list-filters.test.d.ts +2 -0
- package/dist/__tests__/thread-list-filters.test.d.ts.map +1 -0
- package/dist/__tests__/thread-list-filters.test.js +442 -0
- package/dist/__tests__/thread-list-filters.test.js.map +1 -0
- package/dist/__tests__/thread-location.test.d.ts +2 -0
- package/dist/__tests__/thread-location.test.d.ts.map +1 -0
- package/dist/__tests__/thread-location.test.js +159 -0
- package/dist/__tests__/thread-location.test.js.map +1 -0
- package/dist/__tests__/thread-read-quota.test.d.ts +2 -0
- package/dist/__tests__/thread-read-quota.test.d.ts.map +1 -0
- package/dist/__tests__/thread-read-quota.test.js +546 -0
- package/dist/__tests__/thread-read-quota.test.js.map +1 -0
- package/dist/__tests__/thread-read-xml-tags.test.d.ts +2 -0
- package/dist/__tests__/thread-read-xml-tags.test.d.ts.map +1 -0
- package/dist/__tests__/thread-read-xml-tags.test.js +610 -0
- package/dist/__tests__/thread-read-xml-tags.test.js.map +1 -0
- package/dist/__tests__/thread-resume.test.d.ts +2 -0
- package/dist/__tests__/thread-resume.test.d.ts.map +1 -0
- package/dist/__tests__/thread-resume.test.js +592 -0
- package/dist/__tests__/thread-resume.test.js.map +1 -0
- package/dist/__tests__/thread-show-status.test.d.ts +2 -0
- package/dist/__tests__/thread-show-status.test.d.ts.map +1 -0
- package/dist/__tests__/thread-show-status.test.js +267 -0
- package/dist/__tests__/thread-show-status.test.js.map +1 -0
- package/dist/__tests__/thread-start-cwd-cli.test.d.ts +2 -0
- package/dist/__tests__/thread-start-cwd-cli.test.d.ts.map +1 -0
- package/dist/__tests__/thread-start-cwd-cli.test.js +130 -0
- package/dist/__tests__/thread-start-cwd-cli.test.js.map +1 -0
- package/dist/__tests__/thread-step-count.test.d.ts +2 -0
- package/dist/__tests__/thread-step-count.test.d.ts.map +1 -0
- package/dist/__tests__/thread-step-count.test.js +55 -0
- package/dist/__tests__/thread-step-count.test.js.map +1 -0
- package/dist/__tests__/thread-suspend-step.test.d.ts +2 -0
- package/dist/__tests__/thread-suspend-step.test.d.ts.map +1 -0
- package/dist/__tests__/thread-suspend-step.test.js +155 -0
- package/dist/__tests__/thread-suspend-step.test.js.map +1 -0
- package/dist/__tests__/thread-suspended-display.test.d.ts +2 -0
- package/dist/__tests__/thread-suspended-display.test.d.ts.map +1 -0
- package/dist/__tests__/thread-suspended-display.test.js +247 -0
- package/dist/__tests__/thread-suspended-display.test.js.map +1 -0
- package/dist/__tests__/thread-test-helpers.d.ts +4 -0
- package/dist/__tests__/thread-test-helpers.d.ts.map +1 -0
- package/dist/__tests__/thread-test-helpers.js +23 -0
- package/dist/__tests__/thread-test-helpers.js.map +1 -0
- package/dist/__tests__/thread.test.d.ts +2 -0
- package/dist/__tests__/thread.test.d.ts.map +1 -0
- package/dist/__tests__/thread.test.js +883 -0
- package/dist/__tests__/thread.test.js.map +1 -0
- package/dist/__tests__/validate-semantic.test.d.ts +2 -0
- package/dist/__tests__/validate-semantic.test.d.ts.map +1 -0
- package/dist/__tests__/validate-semantic.test.js +408 -0
- package/dist/__tests__/validate-semantic.test.js.map +1 -0
- package/dist/__tests__/workflow-resolution.test.d.ts +2 -0
- package/dist/__tests__/workflow-resolution.test.d.ts.map +1 -0
- package/dist/__tests__/workflow-resolution.test.js +308 -0
- package/dist/__tests__/workflow-resolution.test.js.map +1 -0
- package/dist/background/background.d.ts +38 -0
- package/dist/background/background.d.ts.map +1 -0
- package/dist/background/background.js +123 -0
- package/dist/background/background.js.map +1 -0
- package/dist/background/index.d.ts +3 -0
- package/dist/background/index.d.ts.map +1 -0
- package/dist/background/index.js +2 -0
- package/dist/background/index.js.map +1 -0
- package/dist/background/types.d.ts +9 -0
- package/dist/background/types.d.ts.map +1 -0
- package/dist/background/types.js +2 -0
- package/dist/background/types.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +535 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/config.d.ts +41 -0
- package/dist/commands/config.d.ts.map +1 -0
- package/dist/commands/config.js +252 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/log.d.ts +26 -0
- package/dist/commands/log.d.ts.map +1 -0
- package/dist/commands/log.js +79 -0
- package/dist/commands/log.js.map +1 -0
- package/dist/commands/prompt.d.ts +6 -0
- package/dist/commands/prompt.d.ts.map +1 -0
- package/dist/commands/prompt.js +67 -0
- package/dist/commands/prompt.js.map +1 -0
- package/dist/commands/setup.d.ts +73 -0
- package/dist/commands/setup.d.ts.map +1 -0
- package/dist/commands/setup.js +522 -0
- package/dist/commands/setup.js.map +1 -0
- package/dist/commands/shared.d.ts +31 -0
- package/dist/commands/shared.d.ts.map +1 -0
- package/dist/commands/shared.js +154 -0
- package/dist/commands/shared.js.map +1 -0
- package/dist/commands/step.d.ts +18 -0
- package/dist/commands/step.d.ts.map +1 -0
- package/dist/commands/step.js +257 -0
- package/dist/commands/step.js.map +1 -0
- package/dist/commands/thread-time-parser.d.ts +6 -0
- package/dist/commands/thread-time-parser.d.ts.map +1 -0
- package/dist/commands/thread-time-parser.js +22 -0
- package/dist/commands/thread-time-parser.js.map +1 -0
- package/dist/commands/thread.d.ts +38 -0
- package/dist/commands/thread.d.ts.map +1 -0
- package/dist/commands/thread.js +1087 -0
- package/dist/commands/thread.js.map +1 -0
- package/dist/commands/workflow.d.ts +24 -0
- package/dist/commands/workflow.d.ts.map +1 -0
- package/dist/commands/workflow.js +138 -0
- package/dist/commands/workflow.js.map +1 -0
- package/dist/format.d.ts +3 -0
- package/dist/format.d.ts.map +1 -0
- package/dist/format.js +10 -0
- package/dist/format.js.map +1 -0
- package/dist/include.d.ts +12 -0
- package/dist/include.d.ts.map +1 -0
- package/dist/include.js +35 -0
- package/dist/include.js.map +1 -0
- package/dist/moderator/__tests__/evaluate.test.d.ts +2 -0
- package/dist/moderator/__tests__/evaluate.test.d.ts.map +1 -0
- package/dist/moderator/__tests__/evaluate.test.js +167 -0
- package/dist/moderator/__tests__/evaluate.test.js.map +1 -0
- package/dist/moderator/evaluate.d.ts +6 -0
- package/dist/moderator/evaluate.d.ts.map +1 -0
- package/dist/moderator/evaluate.js +65 -0
- package/dist/moderator/evaluate.js.map +1 -0
- package/dist/moderator/index.d.ts +4 -0
- package/dist/moderator/index.d.ts.map +1 -0
- package/dist/moderator/index.js +3 -0
- package/dist/moderator/index.js.map +1 -0
- package/dist/moderator/types.d.ts +25 -0
- package/dist/moderator/types.d.ts.map +1 -0
- package/dist/moderator/types.js +4 -0
- package/dist/moderator/types.js.map +1 -0
- package/dist/schemas.d.ts +16 -0
- package/dist/schemas.d.ts.map +1 -0
- package/dist/schemas.js +17 -0
- package/dist/schemas.js.map +1 -0
- package/dist/store.d.ts +77 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +392 -0
- package/dist/store.js.map +1 -0
- package/dist/validate-semantic.d.ts +7 -0
- package/dist/validate-semantic.d.ts.map +1 -0
- package/dist/validate-semantic.js +263 -0
- package/dist/validate-semantic.js.map +1 -0
- package/dist/validate.d.ts +16 -0
- package/dist/validate.d.ts.map +1 -0
- package/dist/validate.js +115 -0
- package/dist/validate.js.map +1 -0
- package/package.json +44 -0
- package/src/__tests__/adapter-json-roundtrip.test.ts +181 -0
- package/src/__tests__/config.test.ts +740 -0
- package/src/__tests__/current-role.test.ts +438 -0
- package/src/__tests__/e2e-mock-agent.test.ts +498 -0
- package/src/__tests__/fixtures/e2e-completed-resume.mock.yaml +15 -0
- package/src/__tests__/fixtures/e2e-count.mock.yaml +19 -0
- package/src/__tests__/fixtures/e2e-count.workflow.yaml +45 -0
- package/src/__tests__/fixtures/e2e-linear.mock.yaml +13 -0
- package/src/__tests__/fixtures/e2e-linear.workflow.yaml +32 -0
- package/src/__tests__/fixtures/e2e-loop.mock.yaml +25 -0
- package/src/__tests__/fixtures/e2e-loop.workflow.yaml +36 -0
- package/src/__tests__/fixtures/e2e-mismatch.mock.yaml +16 -0
- package/src/__tests__/fixtures/e2e-mustache.mock.yaml +15 -0
- package/src/__tests__/fixtures/e2e-mustache.workflow.yaml +34 -0
- package/src/__tests__/fixtures/e2e-suspend.mock.yaml +14 -0
- package/src/__tests__/fixtures/e2e-suspend.workflow.yaml +24 -0
- package/src/__tests__/include-tag.test.ts +84 -0
- package/src/__tests__/log.test.ts +181 -0
- package/src/__tests__/moderator-evaluate.test.ts +186 -0
- package/src/__tests__/preload.ts +7 -0
- package/src/__tests__/prompt.test.ts +129 -0
- package/src/__tests__/resolve-head-hash.test.ts +86 -0
- package/src/__tests__/setup-agent-discovery.test.ts +167 -0
- package/src/__tests__/setup-complexity.test.ts +381 -0
- package/src/__tests__/setup-validate.test.ts +148 -0
- package/src/__tests__/solve-issue-tea-worktree.test.ts +144 -0
- package/src/__tests__/spawn-agent-json.test.ts +100 -0
- package/src/__tests__/step-read.test.ts +632 -0
- package/src/__tests__/step-show-json.test.ts +373 -0
- package/src/__tests__/step-timing.test.ts +392 -0
- package/src/__tests__/store-global-cas.test.ts +308 -0
- package/src/__tests__/store-storage-root.test.ts +49 -0
- package/src/__tests__/store-unified-threads.test.ts +235 -0
- package/src/__tests__/thread-cancel-status.test.ts +138 -0
- package/src/__tests__/thread-list-filters.test.ts +572 -0
- package/src/__tests__/thread-location.test.ts +186 -0
- package/src/__tests__/thread-read-quota.test.ts +613 -0
- package/src/__tests__/thread-read-xml-tags.test.ts +717 -0
- package/src/__tests__/thread-resume.test.ts +710 -0
- package/src/__tests__/thread-show-status.test.ts +317 -0
- package/src/__tests__/thread-start-cwd-cli.test.ts +164 -0
- package/src/__tests__/thread-step-count.test.ts +70 -0
- package/src/__tests__/thread-suspend-step.test.ts +181 -0
- package/src/__tests__/thread-suspended-display.test.ts +287 -0
- package/src/__tests__/thread-test-helpers.ts +37 -0
- package/src/__tests__/thread.test.ts +1025 -0
- package/src/__tests__/validate-semantic.test.ts +474 -0
- package/src/__tests__/workflow-resolution.test.ts +421 -0
- package/src/background/background.ts +147 -0
- package/src/background/index.ts +11 -0
- package/src/background/types.ts +9 -0
- package/src/cli.ts +692 -0
- package/src/commands/config.ts +304 -0
- package/src/commands/log.ts +116 -0
- package/src/commands/prompt.ts +81 -0
- package/src/commands/setup.ts +603 -0
- package/src/commands/shared.ts +227 -0
- package/src/commands/step.ts +343 -0
- package/src/commands/thread-time-parser.ts +23 -0
- package/src/commands/thread.ts +1575 -0
- package/src/commands/workflow.ts +213 -0
- package/src/format.ts +12 -0
- package/src/include.ts +37 -0
- package/src/moderator/__tests__/evaluate.test.ts +199 -0
- package/src/moderator/evaluate.ts +80 -0
- package/src/moderator/index.ts +7 -0
- package/src/moderator/types.ts +24 -0
- package/src/schemas.ts +26 -0
- package/src/store.ts +479 -0
- package/src/validate-semantic.ts +304 -0
- package/src/validate.ts +137 -0
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import type { Store as CasStore, JSONSchema } from "@ocas/core";
|
|
2
|
+
import { getSchema } from "@ocas/core";
|
|
3
|
+
import type {
|
|
4
|
+
CasRef,
|
|
5
|
+
StartNodePayload,
|
|
6
|
+
StepNodePayload,
|
|
7
|
+
ThreadId,
|
|
8
|
+
} from "@united-workforce/protocol";
|
|
9
|
+
import { createUwfStore, getThread, type UwfStore } from "../store.js";
|
|
10
|
+
|
|
11
|
+
type ChainState = {
|
|
12
|
+
startHash: CasRef;
|
|
13
|
+
start: StartNodePayload;
|
|
14
|
+
stepsNewestFirst: StepNodePayload[];
|
|
15
|
+
headIsStart: boolean;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
type OrderedStepItem = {
|
|
19
|
+
hash: CasRef;
|
|
20
|
+
payload: StepNodePayload;
|
|
21
|
+
timestamp: number;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
function fail(message: string): never {
|
|
25
|
+
process.stderr.write(`${message}\n`);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function walkChain(uwf: UwfStore, headHash: CasRef): ChainState {
|
|
30
|
+
const headNode = uwf.store.cas.get(headHash);
|
|
31
|
+
if (headNode === null) {
|
|
32
|
+
fail(`CAS node not found: ${headHash}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (headNode.type === uwf.schemas.startNode) {
|
|
36
|
+
return {
|
|
37
|
+
startHash: headHash,
|
|
38
|
+
start: headNode.payload as StartNodePayload,
|
|
39
|
+
stepsNewestFirst: [],
|
|
40
|
+
headIsStart: true,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (headNode.type !== uwf.schemas.stepNode) {
|
|
45
|
+
fail(`head ${headHash} is not a StartNode or StepNode`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const stepsNewestFirst: StepNodePayload[] = [];
|
|
49
|
+
let hash: CasRef | null = headHash;
|
|
50
|
+
|
|
51
|
+
while (hash !== null) {
|
|
52
|
+
const node = uwf.store.cas.get(hash);
|
|
53
|
+
if (node === null) {
|
|
54
|
+
fail(`CAS node not found while walking chain: ${hash}`);
|
|
55
|
+
}
|
|
56
|
+
if (node.type !== uwf.schemas.stepNode) {
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
const payload = node.payload as StepNodePayload;
|
|
60
|
+
stepsNewestFirst.push(payload);
|
|
61
|
+
hash = payload.prev;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const newest = stepsNewestFirst[0];
|
|
65
|
+
if (newest === undefined) {
|
|
66
|
+
fail(`empty step chain at head ${headHash}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const startNode = uwf.store.cas.get(newest.start);
|
|
70
|
+
if (startNode === null || startNode.type !== uwf.schemas.startNode) {
|
|
71
|
+
fail(`StartNode not found: ${newest.start}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
startHash: newest.start,
|
|
76
|
+
start: startNode.payload as StartNodePayload,
|
|
77
|
+
stepsNewestFirst,
|
|
78
|
+
headIsStart: false,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function expandOutput(uwf: UwfStore, outputRef: CasRef): unknown {
|
|
83
|
+
const node = uwf.store.cas.get(outputRef);
|
|
84
|
+
if (node === null) {
|
|
85
|
+
return {};
|
|
86
|
+
}
|
|
87
|
+
return node.payload;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Recursively expand all ocas_ref fields in a CAS node's payload,
|
|
92
|
+
* replacing hash strings with the referenced node's expanded payload.
|
|
93
|
+
*/
|
|
94
|
+
function expandDeep(store: CasStore, hash: CasRef, visited?: Set<string>): unknown {
|
|
95
|
+
const seen = visited ?? new Set<string>();
|
|
96
|
+
if (seen.has(hash)) return hash; // cycle guard
|
|
97
|
+
seen.add(hash);
|
|
98
|
+
|
|
99
|
+
const node = store.cas.get(hash);
|
|
100
|
+
if (node === null) return hash;
|
|
101
|
+
|
|
102
|
+
const schema = getSchema(store, node.type);
|
|
103
|
+
if (schema === null) return node.payload;
|
|
104
|
+
|
|
105
|
+
return expandValue(store, schema, node.payload, seen);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function expandCasRefField(store: CasStore, value: unknown, visited: Set<string>): unknown {
|
|
109
|
+
if (typeof value === "string") {
|
|
110
|
+
return expandDeep(store, value as CasRef, visited);
|
|
111
|
+
}
|
|
112
|
+
return value;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function expandAnyOfField(
|
|
116
|
+
store: CasStore,
|
|
117
|
+
schema: JSONSchema,
|
|
118
|
+
value: unknown,
|
|
119
|
+
visited: Set<string>,
|
|
120
|
+
): unknown {
|
|
121
|
+
if (!Array.isArray(schema.anyOf)) return value;
|
|
122
|
+
for (const sub of schema.anyOf as JSONSchema[]) {
|
|
123
|
+
if (sub.format === "ocas_ref" && typeof value === "string") {
|
|
124
|
+
return expandDeep(store, value as CasRef, visited);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return value;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function expandArrayField(
|
|
131
|
+
store: CasStore,
|
|
132
|
+
schema: JSONSchema,
|
|
133
|
+
value: unknown,
|
|
134
|
+
visited: Set<string>,
|
|
135
|
+
): unknown {
|
|
136
|
+
if (!schema.items || !Array.isArray(value)) return value;
|
|
137
|
+
const itemSchema = schema.items as JSONSchema;
|
|
138
|
+
return (value as unknown[]).map((item) => expandValue(store, itemSchema, item, visited));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function expandObjectField(
|
|
142
|
+
store: CasStore,
|
|
143
|
+
schema: JSONSchema,
|
|
144
|
+
value: unknown,
|
|
145
|
+
visited: Set<string>,
|
|
146
|
+
): unknown {
|
|
147
|
+
if (value === null || typeof value !== "object" || Array.isArray(value) || !schema.properties) {
|
|
148
|
+
return value;
|
|
149
|
+
}
|
|
150
|
+
const props = schema.properties as Record<string, JSONSchema>;
|
|
151
|
+
const obj = value as Record<string, unknown>;
|
|
152
|
+
const result: Record<string, unknown> = {};
|
|
153
|
+
for (const [key, val] of Object.entries(obj)) {
|
|
154
|
+
const propSchema = props[key];
|
|
155
|
+
result[key] = propSchema ? expandValue(store, propSchema, val, visited) : val;
|
|
156
|
+
}
|
|
157
|
+
return result;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function expandValue(
|
|
161
|
+
store: CasStore,
|
|
162
|
+
schema: JSONSchema,
|
|
163
|
+
value: unknown,
|
|
164
|
+
visited: Set<string>,
|
|
165
|
+
): unknown {
|
|
166
|
+
if (schema.format === "ocas_ref") return expandCasRefField(store, value, visited);
|
|
167
|
+
if (Array.isArray(schema.anyOf)) return expandAnyOfField(store, schema, value, visited);
|
|
168
|
+
if (schema.type === "array") return expandArrayField(store, schema, value, visited);
|
|
169
|
+
return expandObjectField(store, schema, value, visited);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function collectOrderedSteps(
|
|
173
|
+
uwf: UwfStore,
|
|
174
|
+
headHash: CasRef,
|
|
175
|
+
chain: ChainState,
|
|
176
|
+
): OrderedStepItem[] {
|
|
177
|
+
let hash: CasRef | null = headHash;
|
|
178
|
+
const hashToNode = new Map<string, { payload: StepNodePayload; timestamp: number }>();
|
|
179
|
+
while (hash !== null) {
|
|
180
|
+
const node = uwf.store.cas.get(hash);
|
|
181
|
+
if (node === null || node.type !== uwf.schemas.stepNode) {
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
const payload = node.payload as StepNodePayload;
|
|
185
|
+
hashToNode.set(hash, { payload, timestamp: node.timestamp });
|
|
186
|
+
hash = payload.prev;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
let cur: CasRef | null = chain.headIsStart ? null : headHash;
|
|
190
|
+
const ordered: OrderedStepItem[] = [];
|
|
191
|
+
while (cur !== null) {
|
|
192
|
+
const entry = hashToNode.get(cur);
|
|
193
|
+
if (entry === undefined) {
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
ordered.push({ hash: cur, ...entry });
|
|
197
|
+
cur = entry.payload.prev;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
ordered.reverse();
|
|
201
|
+
return ordered;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async function resolveHeadHash(storageRoot: string, threadId: ThreadId): Promise<CasRef> {
|
|
205
|
+
const uwf = await createUwfStore(storageRoot);
|
|
206
|
+
const entry = getThread(uwf.varStore, threadId);
|
|
207
|
+
if (entry !== null) {
|
|
208
|
+
return entry.head;
|
|
209
|
+
}
|
|
210
|
+
fail(`thread not found: ${threadId}`);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export {
|
|
214
|
+
type ChainState,
|
|
215
|
+
collectOrderedSteps,
|
|
216
|
+
expandAnyOfField,
|
|
217
|
+
expandArrayField,
|
|
218
|
+
expandCasRefField,
|
|
219
|
+
expandDeep,
|
|
220
|
+
expandObjectField,
|
|
221
|
+
expandOutput,
|
|
222
|
+
expandValue,
|
|
223
|
+
fail,
|
|
224
|
+
type OrderedStepItem,
|
|
225
|
+
resolveHeadHash,
|
|
226
|
+
walkChain,
|
|
227
|
+
};
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
import type { CasStore } from "@ocas/core";
|
|
2
|
+
import type {
|
|
3
|
+
CasRef,
|
|
4
|
+
StartEntry,
|
|
5
|
+
StepEntry,
|
|
6
|
+
StepNodePayload,
|
|
7
|
+
ThreadForkOutput,
|
|
8
|
+
ThreadId,
|
|
9
|
+
ThreadStepsOutput,
|
|
10
|
+
} from "@united-workforce/protocol";
|
|
11
|
+
import { generateUlid } from "@united-workforce/util";
|
|
12
|
+
import { createUwfStore, setThread } from "../store.js";
|
|
13
|
+
import {
|
|
14
|
+
collectOrderedSteps,
|
|
15
|
+
expandDeep,
|
|
16
|
+
expandOutput,
|
|
17
|
+
fail,
|
|
18
|
+
resolveHeadHash,
|
|
19
|
+
walkChain,
|
|
20
|
+
} from "./shared.js";
|
|
21
|
+
|
|
22
|
+
type TurnToolCall = {
|
|
23
|
+
name: string;
|
|
24
|
+
args: string;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
type TurnData = {
|
|
28
|
+
index: number;
|
|
29
|
+
role: string;
|
|
30
|
+
content: string;
|
|
31
|
+
toolCalls: TurnToolCall[] | null;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* List all steps in a thread (previously: thread steps)
|
|
36
|
+
*/
|
|
37
|
+
export async function cmdStepList(
|
|
38
|
+
storageRoot: string,
|
|
39
|
+
threadId: ThreadId,
|
|
40
|
+
): Promise<ThreadStepsOutput> {
|
|
41
|
+
const headHash = await resolveHeadHash(storageRoot, threadId);
|
|
42
|
+
const uwf = await createUwfStore(storageRoot);
|
|
43
|
+
const chain = walkChain(uwf, headHash);
|
|
44
|
+
|
|
45
|
+
const startNode = uwf.store.cas.get(chain.startHash);
|
|
46
|
+
if (startNode === null) {
|
|
47
|
+
fail(`StartNode not found: ${chain.startHash}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const startEntry: StartEntry = {
|
|
51
|
+
hash: chain.startHash,
|
|
52
|
+
workflow: chain.start.workflow,
|
|
53
|
+
prompt: chain.start.prompt,
|
|
54
|
+
timestamp: startNode.timestamp,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const stepEntries: StepEntry[] = [];
|
|
58
|
+
const ordered = collectOrderedSteps(uwf, headHash, chain);
|
|
59
|
+
|
|
60
|
+
for (const item of ordered) {
|
|
61
|
+
stepEntries.push({
|
|
62
|
+
hash: item.hash,
|
|
63
|
+
role: item.payload.role,
|
|
64
|
+
output: expandOutput(uwf, item.payload.output),
|
|
65
|
+
detail: item.payload.detail ?? null,
|
|
66
|
+
agent: item.payload.agent,
|
|
67
|
+
timestamp: item.timestamp,
|
|
68
|
+
durationMs: item.payload.completedAtMs - item.payload.startedAtMs,
|
|
69
|
+
usage: item.payload.usage ?? null,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
thread: threadId,
|
|
75
|
+
workflow: chain.start.workflow,
|
|
76
|
+
steps: [startEntry, ...stepEntries],
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Show details of a specific step (previously: thread step-details)
|
|
82
|
+
*/
|
|
83
|
+
export async function cmdStepShow(storageRoot: string, stepHash: CasRef): Promise<unknown> {
|
|
84
|
+
const uwf = await createUwfStore(storageRoot);
|
|
85
|
+
const node = uwf.store.cas.get(stepHash);
|
|
86
|
+
if (node === null) {
|
|
87
|
+
fail(`CAS node not found: ${stepHash}`);
|
|
88
|
+
}
|
|
89
|
+
if (node.type !== uwf.schemas.stepNode) {
|
|
90
|
+
fail(`node ${stepHash} is not a StepNode`);
|
|
91
|
+
}
|
|
92
|
+
const payload = node.payload as StepNodePayload;
|
|
93
|
+
if (!payload.detail) {
|
|
94
|
+
fail(`step ${stepHash} has no detail`);
|
|
95
|
+
}
|
|
96
|
+
return expandDeep(uwf.store, payload.detail);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Fork a thread from a specific step (previously: thread fork)
|
|
101
|
+
*/
|
|
102
|
+
export async function cmdStepFork(
|
|
103
|
+
storageRoot: string,
|
|
104
|
+
stepHash: CasRef,
|
|
105
|
+
): Promise<ThreadForkOutput> {
|
|
106
|
+
const uwf = await createUwfStore(storageRoot);
|
|
107
|
+
const node = uwf.store.cas.get(stepHash);
|
|
108
|
+
if (node === null) {
|
|
109
|
+
fail(`CAS node not found: ${stepHash}`);
|
|
110
|
+
}
|
|
111
|
+
if (node.type !== uwf.schemas.startNode && node.type !== uwf.schemas.stepNode) {
|
|
112
|
+
fail(`node ${stepHash} is not a StartNode or StepNode`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const newThreadId = generateUlid(Date.now()) as ThreadId;
|
|
116
|
+
setThread(uwf.varStore, newThreadId, {
|
|
117
|
+
head: stepHash,
|
|
118
|
+
status: "idle",
|
|
119
|
+
suspendedRole: null,
|
|
120
|
+
suspendMessage: null,
|
|
121
|
+
completedAt: null,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
thread: newThreadId,
|
|
126
|
+
forkedFrom: {
|
|
127
|
+
step: stepHash,
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Load and validate step detail node from CAS store
|
|
134
|
+
*/
|
|
135
|
+
function loadStepDetail(store: CasStore, detailRef: CasRef): Record<string, unknown> {
|
|
136
|
+
const detailNode = store.get(detailRef);
|
|
137
|
+
if (detailNode === null) {
|
|
138
|
+
fail(`detail node not found: ${detailRef}`);
|
|
139
|
+
}
|
|
140
|
+
return detailNode.payload as Record<string, unknown>;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function parseTurnToolCalls(raw: unknown): TurnToolCall[] | null {
|
|
144
|
+
if (!Array.isArray(raw) || raw.length === 0) {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
const calls: TurnToolCall[] = [];
|
|
148
|
+
for (const entry of raw) {
|
|
149
|
+
if (typeof entry !== "object" || entry === null) {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
const record = entry as Record<string, unknown>;
|
|
153
|
+
const name = record.name;
|
|
154
|
+
const args = record.args;
|
|
155
|
+
if (typeof name === "string") {
|
|
156
|
+
calls.push({ name, args: typeof args === "string" ? args : "" });
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return calls.length > 0 ? calls : null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function formatTurnBody(turn: TurnData): string {
|
|
163
|
+
const parts: string[] = [];
|
|
164
|
+
parts.push(`**Turn role:** ${turn.role}`);
|
|
165
|
+
|
|
166
|
+
if (turn.toolCalls !== null) {
|
|
167
|
+
for (const call of turn.toolCalls) {
|
|
168
|
+
const argsSuffix = call.args !== "" ? ` — \`${call.args}\`` : "";
|
|
169
|
+
parts.push(`- **${call.name}**${argsSuffix}`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (turn.content !== "") {
|
|
174
|
+
if (parts.length > 0) {
|
|
175
|
+
parts.push("");
|
|
176
|
+
}
|
|
177
|
+
parts.push(turn.content);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return parts.join("\n");
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function parseSingleTurn(
|
|
184
|
+
store: CasStore,
|
|
185
|
+
turnRef: unknown,
|
|
186
|
+
fallbackIndex: number,
|
|
187
|
+
): TurnData | null {
|
|
188
|
+
if (typeof turnRef !== "string") {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
const turnNode = store.get(turnRef as CasRef);
|
|
192
|
+
if (turnNode === null) {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
const turn = turnNode.payload as Record<string, unknown>;
|
|
196
|
+
const content = typeof turn.content === "string" ? turn.content : "";
|
|
197
|
+
const toolCalls = parseTurnToolCalls(turn.toolCalls);
|
|
198
|
+
if (content === "" && toolCalls === null) {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
return {
|
|
202
|
+
index: typeof turn.index === "number" ? turn.index : fallbackIndex,
|
|
203
|
+
role: typeof turn.role === "string" ? turn.role : "assistant",
|
|
204
|
+
content,
|
|
205
|
+
toolCalls,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Load all turn nodes from CAS store and extract display fields
|
|
211
|
+
*/
|
|
212
|
+
function loadTurnData(store: CasStore, turns: unknown): TurnData[] {
|
|
213
|
+
if (!Array.isArray(turns) || turns.length === 0) {
|
|
214
|
+
return [];
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const turnData: TurnData[] = [];
|
|
218
|
+
for (const turnRef of turns) {
|
|
219
|
+
const parsed = parseSingleTurn(store, turnRef, turnData.length);
|
|
220
|
+
if (parsed !== null) {
|
|
221
|
+
turnData.push(parsed);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return turnData;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Select turns that fit within quota, working backwards from most recent
|
|
229
|
+
*/
|
|
230
|
+
function selectTurnsForQuota(turnData: TurnData[], availableQuota: number): TurnData[] {
|
|
231
|
+
const selectedTurns: TurnData[] = [];
|
|
232
|
+
let totalChars = 0;
|
|
233
|
+
|
|
234
|
+
for (let i = turnData.length - 1; i >= 0; i--) {
|
|
235
|
+
const turn = turnData[i];
|
|
236
|
+
if (turn === undefined) continue;
|
|
237
|
+
|
|
238
|
+
const turnHeader = `## Turn ${turn.index + 1}\n\n`;
|
|
239
|
+
const turnBlock = turnHeader + formatTurnBody(turn);
|
|
240
|
+
const separatorCost = selectedTurns.length > 0 ? 2 : 0;
|
|
241
|
+
const addCost = turnBlock.length + separatorCost;
|
|
242
|
+
|
|
243
|
+
if (totalChars + addCost > availableQuota && selectedTurns.length > 0) {
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
selectedTurns.unshift(turn);
|
|
248
|
+
totalChars += addCost;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return selectedTurns;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Assemble final markdown output from header and selected turns
|
|
256
|
+
*/
|
|
257
|
+
function formatStepMarkdown(
|
|
258
|
+
stepHash: CasRef,
|
|
259
|
+
role: string,
|
|
260
|
+
agent: string,
|
|
261
|
+
turnData: TurnData[],
|
|
262
|
+
selectedTurns: TurnData[],
|
|
263
|
+
): string {
|
|
264
|
+
const parts: string[] = [];
|
|
265
|
+
parts.push(`# Step ${stepHash}`);
|
|
266
|
+
parts.push("");
|
|
267
|
+
parts.push(`**Role:** ${role}`);
|
|
268
|
+
parts.push(`**Agent:** ${agent}`);
|
|
269
|
+
|
|
270
|
+
if (selectedTurns.length === 0) {
|
|
271
|
+
return parts.join("\n");
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const skippedCount = turnData.length - selectedTurns.length;
|
|
275
|
+
if (skippedCount > 0) {
|
|
276
|
+
parts.push("");
|
|
277
|
+
parts.push(`_[Earlier turns omitted due to quota. Use --quota to increase.]_`);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
for (const turn of selectedTurns) {
|
|
281
|
+
parts.push("");
|
|
282
|
+
parts.push(`## Turn ${turn.index + 1}`);
|
|
283
|
+
parts.push("");
|
|
284
|
+
parts.push(formatTurnBody(turn));
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return parts.join("\n");
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Read a step's agent turns as human-readable markdown with quota enforcement
|
|
292
|
+
*/
|
|
293
|
+
export async function cmdStepRead(
|
|
294
|
+
storageRoot: string,
|
|
295
|
+
stepHash: CasRef,
|
|
296
|
+
quota: number,
|
|
297
|
+
showPrompt: boolean,
|
|
298
|
+
): Promise<string> {
|
|
299
|
+
const uwf = await createUwfStore(storageRoot);
|
|
300
|
+
const node = uwf.store.cas.get(stepHash);
|
|
301
|
+
if (node === null) {
|
|
302
|
+
fail(`CAS node not found: ${stepHash}`);
|
|
303
|
+
}
|
|
304
|
+
if (node.type !== uwf.schemas.stepNode) {
|
|
305
|
+
fail(`node ${stepHash} is not a StepNode`);
|
|
306
|
+
}
|
|
307
|
+
const payload = node.payload as StepNodePayload;
|
|
308
|
+
|
|
309
|
+
// --prompt mode: show the assembled prompt that was sent to the agent
|
|
310
|
+
if (showPrompt) {
|
|
311
|
+
const promptRef = (payload as Record<string, unknown>).assembledPrompt;
|
|
312
|
+
if (typeof promptRef !== "string") {
|
|
313
|
+
return `# Step ${stepHash}\n\n_Prompt not recorded (legacy step)._`;
|
|
314
|
+
}
|
|
315
|
+
const promptNode = uwf.store.cas.get(promptRef as CasRef);
|
|
316
|
+
if (promptNode === null) {
|
|
317
|
+
return `# Step ${stepHash}\n\n_Prompt CAS node not found: ${promptRef}_`;
|
|
318
|
+
}
|
|
319
|
+
const promptText =
|
|
320
|
+
typeof promptNode.payload === "string"
|
|
321
|
+
? promptNode.payload
|
|
322
|
+
: JSON.stringify(promptNode.payload);
|
|
323
|
+
return `# Step ${stepHash}\n\n**Role:** ${payload.role}\n**Agent:** ${payload.agent}\n\n## Prompt\n\n${promptText}`;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (payload.detail === null) {
|
|
327
|
+
return formatStepMarkdown(stepHash, payload.role, payload.agent, [], []);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const detail = loadStepDetail(uwf.store.cas, payload.detail);
|
|
331
|
+
const turnData = loadTurnData(uwf.store.cas, detail.turns);
|
|
332
|
+
|
|
333
|
+
if (turnData.length === 0) {
|
|
334
|
+
return formatStepMarkdown(stepHash, payload.role, payload.agent, [], []);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const headerSection = formatStepMarkdown(stepHash, payload.role, payload.agent, [], []);
|
|
338
|
+
const BUFFER = 200;
|
|
339
|
+
const availableQuota = quota - headerSection.length - BUFFER;
|
|
340
|
+
const selectedTurns = selectTurnsForQuota(turnData, availableQuota);
|
|
341
|
+
|
|
342
|
+
return formatStepMarkdown(stepHash, payload.role, payload.agent, turnData, selectedTurns);
|
|
343
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse time input: ISO date (YYYY-MM-DD, YYYY-MM-DDTHH:MM:SS) or relative (7d, 24h, 30m)
|
|
3
|
+
* Returns Unix timestamp in milliseconds.
|
|
4
|
+
*/
|
|
5
|
+
export function parseTimeInput(input: string, nowMs: number): number {
|
|
6
|
+
const trimmed = input.trim();
|
|
7
|
+
|
|
8
|
+
// Relative time: 7d, 24h, 30m
|
|
9
|
+
const relativeMatch = /^(\d+)(d|h|m)$/.exec(trimmed);
|
|
10
|
+
if (relativeMatch !== null) {
|
|
11
|
+
const value = Number.parseInt(relativeMatch[1], 10);
|
|
12
|
+
const unit = relativeMatch[2];
|
|
13
|
+
const multiplier = unit === "d" ? 86400000 : unit === "h" ? 3600000 : 60000;
|
|
14
|
+
return nowMs - value * multiplier;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// ISO date: try parsing
|
|
18
|
+
const parsed = Date.parse(trimmed);
|
|
19
|
+
if (Number.isNaN(parsed)) {
|
|
20
|
+
throw new Error(`invalid time format: ${trimmed} (expected ISO date or relative like '7d')`);
|
|
21
|
+
}
|
|
22
|
+
return parsed;
|
|
23
|
+
}
|