@tagma/sdk 0.5.2 → 0.6.1

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/src/sdk.ts CHANGED
@@ -1,118 +1,118 @@
1
- // ═══ tagma-sdk public API ═══
2
- //
3
- // This is the SDK entry point. Import from here, not from internal modules.
4
- // The CLI (src/index.ts in the CLI project) also imports from here.
5
-
6
- // ── Core engine ──
7
- export { runPipeline, TriggerBlockedError, TriggerTimeoutError } from './engine';
8
- export type { EngineResult, RunPipelineOptions, RunEventPayload } from './engine';
9
-
10
- // ── Pipeline runner (multi-pipeline lifecycle management) ──
11
- export { PipelineRunner } from './pipeline-runner';
12
- export type { PipelineRunnerStatus } from './pipeline-runner';
13
-
14
- // ── Raw config CRUD (visual editor / YAML sync) ──
15
- export {
16
- createEmptyPipeline,
17
- setPipelineField,
18
- upsertTrack,
19
- removeTrack,
20
- moveTrack,
21
- updateTrack,
22
- upsertTask,
23
- removeTask,
24
- moveTask,
25
- transferTask,
26
- } from './config-ops';
27
-
28
- // ── Raw config validation (real-time feedback) ──
29
- export { validateRaw } from './validate-raw';
30
- export type { ValidationError } from './validate-raw';
31
-
32
- // ── Schema: parse / resolve / load / serialize / validate ──
33
- export {
34
- parseYaml,
35
- resolveConfig,
36
- loadPipeline,
37
- serializePipeline,
38
- deresolvePipeline,
39
- validateConfig,
40
- } from './schema';
41
-
42
- // ── DAG ──
43
- export { buildDag, buildRawDag } from './dag';
44
- export type { DagNode, Dag, RawDagNode, RawDag } from './dag';
45
-
46
- // ── Plugin registry ──
47
- export { bootstrapBuiltins } from './bootstrap';
48
- export {
49
- loadPlugins,
50
- registerPlugin,
51
- unregisterPlugin,
52
- getHandler,
53
- hasHandler,
54
- listRegistered,
55
- isValidPluginName,
56
- PLUGIN_NAME_RE,
57
- readPluginManifest,
58
- } from './registry';
59
- export type { RegisterResult } from './registry';
60
-
61
- // ── Approval gateway ──
62
- export { InMemoryApprovalGateway } from './approval';
63
- export type {
64
- ApprovalGateway,
65
- ApprovalRequest,
66
- ApprovalDecision,
67
- ApprovalOutcome,
68
- ApprovalEvent,
69
- ApprovalListener,
70
- } from './approval';
71
-
72
- // ── Approval adapters ──
73
- export { attachStdinApprovalAdapter } from './adapters/stdin-approval';
74
- export type { StdinApprovalAdapter } from './adapters/stdin-approval';
75
- export { attachWebSocketApprovalAdapter } from './adapters/websocket-approval';
76
- export type {
77
- WebSocketApprovalAdapter,
78
- WebSocketApprovalAdapterOptions,
79
- } from './adapters/websocket-approval';
80
-
81
- // ── Logger ──
82
- export { Logger, tailLines, clip } from './logger';
83
- export type { LogRecord, LogLevel, LogListener } from './logger';
84
-
85
- // ── Hook context types (useful for frontend display) ──
86
- export type { HookResult, PipelineInfo, TrackInfo, TaskInfo } from './hooks';
87
-
88
- // ── Utils (public subset) ──
89
- export {
90
- parseDuration,
91
- validatePath,
92
- generateRunId,
93
- nowISO,
94
- truncateForName,
95
- _resetShellCache,
96
- } from './utils';
97
-
98
- // ── Task reference resolution (shared id normalization) ──
99
- export {
100
- TASK_ID_RE,
101
- isValidTaskId,
102
- qualifyTaskId,
103
- isQualifiedRef,
104
- buildTaskIndex,
105
- resolveTaskRef,
106
- AMBIGUOUS,
107
- } from './task-ref';
108
- export type { TaskIndex, RefResolution } from './task-ref';
109
-
110
- // ── Prompt document helpers (middleware authors + drivers) ──
111
- export {
112
- promptDocumentFromString,
113
- serializePromptDocument,
114
- appendContext,
115
- } from './prompt-doc';
116
-
117
- // ── All types from @tagma/types + runtime constants ──
118
- export * from './types';
1
+ // ═══ tagma-sdk public API ═══
2
+ //
3
+ // This is the SDK entry point. Import from here, not from internal modules.
4
+ // The CLI (src/index.ts in the CLI project) also imports from here.
5
+
6
+ // ── Core engine ──
7
+ export { runPipeline, TriggerBlockedError, TriggerTimeoutError } from './engine';
8
+ export type { EngineResult, RunPipelineOptions, RunEventPayload } from './engine';
9
+
10
+ // ── Pipeline runner (multi-pipeline lifecycle management) ──
11
+ export { PipelineRunner } from './pipeline-runner';
12
+ export type { PipelineRunnerStatus } from './pipeline-runner';
13
+
14
+ // ── Raw config CRUD (visual editor / YAML sync) ──
15
+ export {
16
+ createEmptyPipeline,
17
+ setPipelineField,
18
+ upsertTrack,
19
+ removeTrack,
20
+ moveTrack,
21
+ updateTrack,
22
+ upsertTask,
23
+ removeTask,
24
+ moveTask,
25
+ transferTask,
26
+ } from './config-ops';
27
+
28
+ // ── Raw config validation (real-time feedback) ──
29
+ export { validateRaw } from './validate-raw';
30
+ export type { ValidationError } from './validate-raw';
31
+
32
+ // ── Schema: parse / resolve / load / serialize / validate ──
33
+ export {
34
+ parseYaml,
35
+ resolveConfig,
36
+ loadPipeline,
37
+ serializePipeline,
38
+ deresolvePipeline,
39
+ validateConfig,
40
+ } from './schema';
41
+
42
+ // ── DAG ──
43
+ export { buildDag, buildRawDag } from './dag';
44
+ export type { DagNode, Dag, RawDagNode, RawDag } from './dag';
45
+
46
+ // ── Plugin registry ──
47
+ export { bootstrapBuiltins } from './bootstrap';
48
+ export {
49
+ loadPlugins,
50
+ registerPlugin,
51
+ unregisterPlugin,
52
+ getHandler,
53
+ hasHandler,
54
+ listRegistered,
55
+ isValidPluginName,
56
+ PLUGIN_NAME_RE,
57
+ readPluginManifest,
58
+ } from './registry';
59
+ export type { RegisterResult } from './registry';
60
+
61
+ // ── Approval gateway ──
62
+ export { InMemoryApprovalGateway } from './approval';
63
+ export type {
64
+ ApprovalGateway,
65
+ ApprovalRequest,
66
+ ApprovalDecision,
67
+ ApprovalOutcome,
68
+ ApprovalEvent,
69
+ ApprovalListener,
70
+ } from './approval';
71
+
72
+ // ── Approval adapters ──
73
+ export { attachStdinApprovalAdapter } from './adapters/stdin-approval';
74
+ export type { StdinApprovalAdapter } from './adapters/stdin-approval';
75
+ export { attachWebSocketApprovalAdapter } from './adapters/websocket-approval';
76
+ export type {
77
+ WebSocketApprovalAdapter,
78
+ WebSocketApprovalAdapterOptions,
79
+ } from './adapters/websocket-approval';
80
+
81
+ // ── Logger ──
82
+ export { Logger, tailLines, clip } from './logger';
83
+ export type { LogRecord, LogLevel, LogListener } from './logger';
84
+
85
+ // ── Hook context types (useful for frontend display) ──
86
+ export type { HookResult, PipelineInfo, TrackInfo, TaskInfo } from './hooks';
87
+
88
+ // ── Utils (public subset) ──
89
+ export {
90
+ parseDuration,
91
+ validatePath,
92
+ generateRunId,
93
+ nowISO,
94
+ truncateForName,
95
+ _resetShellCache,
96
+ } from './utils';
97
+
98
+ // ── Task reference resolution (shared id normalization) ──
99
+ export {
100
+ TASK_ID_RE,
101
+ isValidTaskId,
102
+ qualifyTaskId,
103
+ isQualifiedRef,
104
+ buildTaskIndex,
105
+ resolveTaskRef,
106
+ AMBIGUOUS,
107
+ } from './task-ref';
108
+ export type { TaskIndex, RefResolution } from './task-ref';
109
+
110
+ // ── Prompt document helpers (middleware authors + drivers) ──
111
+ export {
112
+ promptDocumentFromString,
113
+ serializePromptDocument,
114
+ appendContext,
115
+ } from './prompt-doc';
116
+
117
+ // ── All types from @tagma/types + runtime constants ──
118
+ export * from './types';
@@ -1,164 +1,164 @@
1
- import { watch } from 'chokidar';
2
- import { resolve, dirname } from 'path';
3
- import { mkdir } from 'fs/promises';
4
- import type { TriggerPlugin, TriggerContext } from '../types';
5
- import { parseDuration, validatePath } from '../utils';
6
- import { TriggerTimeoutError } from '../engine';
7
-
8
- const IS_WINDOWS = process.platform === 'win32';
9
-
10
- function pathsEqual(a: string, b: string): boolean {
11
- return IS_WINDOWS ? a.toLowerCase() === b.toLowerCase() : a === b;
12
- }
13
-
14
- export const FileTrigger: TriggerPlugin = {
15
- name: 'file',
16
- schema: {
17
- description: 'Wait for a file to appear or be modified before the task runs.',
18
- fields: {
19
- path: {
20
- type: 'path',
21
- required: true,
22
- description: 'Path to the file to watch (relative to workDir or absolute).',
23
- placeholder: 'e.g. build/output.json',
24
- },
25
- timeout: {
26
- type: 'duration',
27
- description: 'Maximum wait time (e.g. 30s, 5m). Omit or 0 to wait indefinitely.',
28
- placeholder: '30s',
29
- },
30
- },
31
- },
32
-
33
- watch(config: Record<string, unknown>, ctx: TriggerContext): Promise<unknown> {
34
- const filePath = config.path as string;
35
- if (!filePath) throw new Error(`file trigger: "path" is required`);
36
-
37
- const safePath = validatePath(filePath, ctx.workDir);
38
- const timeoutMs = config.timeout != null ? parseDuration(String(config.timeout)) : 0;
39
-
40
- // Hoist the async work into a named async function so the Promise
41
- // constructor itself is synchronous — avoids the no-async-promise-executor
42
- // lint error and ensures exceptions are always propagated via reject().
43
- async function start(
44
- resolve_p: (value: unknown) => void,
45
- reject: (reason?: unknown) => void,
46
- ): Promise<void> {
47
- if (ctx.signal.aborted) {
48
- reject(new Error('Pipeline aborted'));
49
- return;
50
- }
51
-
52
- let settled = false;
53
- let timer: ReturnType<typeof setTimeout> | null = null;
54
-
55
- // Ensure the parent directory exists so the watcher doesn't fail
56
- // with ENOENT for nested paths like `build/output/result.json`.
57
- const dir = dirname(safePath);
58
- try {
59
- await mkdir(dir, { recursive: true });
60
- } catch {
61
- /* best effort — dir may already exist */
62
- }
63
-
64
- // Pass `cwd: dir` so chokidar resolves paths relative to the watched
65
- // directory. The 'add'/'change' events will then carry paths relative
66
- // to `dir`, which we resolve with `resolve(dir, addedPath)` for an
67
- // accurate absolute comparison — fixing the ambiguous process.cwd()
68
- // resolution of the previous implementation.
69
- const watcher = watch(dir, {
70
- ignoreInitial: true,
71
- depth: 0,
72
- cwd: dir,
73
- awaitWriteFinish: { stabilityThreshold: 100, pollInterval: 50 },
74
- });
75
-
76
- const cleanup = () => {
77
- if (settled) return;
78
- settled = true;
79
- watcher.close().catch(() => {
80
- /* ignore */
81
- });
82
- if (timer) clearTimeout(timer);
83
- ctx.signal.removeEventListener('abort', onAbort);
84
- };
85
-
86
- const onAbort = () => {
87
- cleanup();
88
- reject(new Error('Pipeline aborted'));
89
- };
90
-
91
- watcher.on('add', (addedPath: string) => {
92
- if (settled) return;
93
- if (pathsEqual(resolve(dir, addedPath), safePath)) {
94
- cleanup();
95
- resolve_p({ path: safePath });
96
- }
97
- });
98
-
99
- // Also fire on 'change' so that overwriting an existing file is detected.
100
- // Without this, upstream tasks that truncate-and-rewrite a file emit only
101
- // a 'change' event and the downstream trigger would never resolve.
102
- watcher.on('change', (changedPath: string) => {
103
- if (settled) return;
104
- if (pathsEqual(resolve(dir, changedPath), safePath)) {
105
- cleanup();
106
- resolve_p({ path: safePath });
107
- }
108
- });
109
-
110
- watcher.on('error', (err: unknown) => {
111
- if (settled) return;
112
- cleanup();
113
- reject(
114
- new Error(
115
- `file trigger watch error: ${err instanceof Error ? err.message : String(err)}`,
116
- ),
117
- );
118
- });
119
-
120
- // After the watcher finishes its initial scan, check if the file already exists.
121
- // Doing this inside 'ready' eliminates the race window between existence check
122
- // and watcher startup, so we neither miss events nor double-resolve.
123
- watcher.on('ready', () => {
124
- if (settled) return;
125
- Bun.file(safePath)
126
- .exists()
127
- .then((exists) => {
128
- if (settled) return;
129
- if (exists) {
130
- cleanup();
131
- resolve_p({ path: safePath });
132
- }
133
- })
134
- .catch((err: unknown) => {
135
- if (settled) return;
136
- cleanup();
137
- reject(
138
- new Error(
139
- `file trigger existence check failed: ${err instanceof Error ? err.message : String(err)}`,
140
- ),
141
- );
142
- });
143
- });
144
-
145
- if (timeoutMs > 0) {
146
- timer = setTimeout(() => {
147
- if (settled) return;
148
- cleanup();
149
- reject(
150
- new TriggerTimeoutError(
151
- `file trigger timeout: ${filePath} did not appear within ${config.timeout}`,
152
- ),
153
- );
154
- }, timeoutMs);
155
- }
156
-
157
- ctx.signal.addEventListener('abort', onAbort);
158
- }
159
-
160
- return new Promise((resolve_p, reject) => {
161
- start(resolve_p, reject).catch(reject);
162
- });
163
- },
164
- };
1
+ import { watch } from 'chokidar';
2
+ import { resolve, dirname } from 'path';
3
+ import { mkdir } from 'fs/promises';
4
+ import type { TriggerPlugin, TriggerContext } from '../types';
5
+ import { parseDuration, validatePath } from '../utils';
6
+ import { TriggerTimeoutError } from '../engine';
7
+
8
+ const IS_WINDOWS = process.platform === 'win32';
9
+
10
+ function pathsEqual(a: string, b: string): boolean {
11
+ return IS_WINDOWS ? a.toLowerCase() === b.toLowerCase() : a === b;
12
+ }
13
+
14
+ export const FileTrigger: TriggerPlugin = {
15
+ name: 'file',
16
+ schema: {
17
+ description: 'Wait for a file to appear or be modified before the task runs.',
18
+ fields: {
19
+ path: {
20
+ type: 'path',
21
+ required: true,
22
+ description: 'Path to the file to watch (relative to workDir or absolute).',
23
+ placeholder: 'e.g. build/output.json',
24
+ },
25
+ timeout: {
26
+ type: 'duration',
27
+ description: 'Maximum wait time (e.g. 30s, 5m). Omit or 0 to wait indefinitely.',
28
+ placeholder: '30s',
29
+ },
30
+ },
31
+ },
32
+
33
+ watch(config: Record<string, unknown>, ctx: TriggerContext): Promise<unknown> {
34
+ const filePath = config.path as string;
35
+ if (!filePath) throw new Error(`file trigger: "path" is required`);
36
+
37
+ const safePath = validatePath(filePath, ctx.workDir);
38
+ const timeoutMs = config.timeout != null ? parseDuration(String(config.timeout)) : 0;
39
+
40
+ // Hoist the async work into a named async function so the Promise
41
+ // constructor itself is synchronous — avoids the no-async-promise-executor
42
+ // lint error and ensures exceptions are always propagated via reject().
43
+ async function start(
44
+ resolve_p: (value: unknown) => void,
45
+ reject: (reason?: unknown) => void,
46
+ ): Promise<void> {
47
+ if (ctx.signal.aborted) {
48
+ reject(new Error('Pipeline aborted'));
49
+ return;
50
+ }
51
+
52
+ let settled = false;
53
+ let timer: ReturnType<typeof setTimeout> | null = null;
54
+
55
+ // Ensure the parent directory exists so the watcher doesn't fail
56
+ // with ENOENT for nested paths like `build/output/result.json`.
57
+ const dir = dirname(safePath);
58
+ try {
59
+ await mkdir(dir, { recursive: true });
60
+ } catch {
61
+ /* best effort — dir may already exist */
62
+ }
63
+
64
+ // Pass `cwd: dir` so chokidar resolves paths relative to the watched
65
+ // directory. The 'add'/'change' events will then carry paths relative
66
+ // to `dir`, which we resolve with `resolve(dir, addedPath)` for an
67
+ // accurate absolute comparison — fixing the ambiguous process.cwd()
68
+ // resolution of the previous implementation.
69
+ const watcher = watch(dir, {
70
+ ignoreInitial: true,
71
+ depth: 0,
72
+ cwd: dir,
73
+ awaitWriteFinish: { stabilityThreshold: 100, pollInterval: 50 },
74
+ });
75
+
76
+ const cleanup = () => {
77
+ if (settled) return;
78
+ settled = true;
79
+ watcher.close().catch(() => {
80
+ /* ignore */
81
+ });
82
+ if (timer) clearTimeout(timer);
83
+ ctx.signal.removeEventListener('abort', onAbort);
84
+ };
85
+
86
+ const onAbort = () => {
87
+ cleanup();
88
+ reject(new Error('Pipeline aborted'));
89
+ };
90
+
91
+ watcher.on('add', (addedPath: string) => {
92
+ if (settled) return;
93
+ if (pathsEqual(resolve(dir, addedPath), safePath)) {
94
+ cleanup();
95
+ resolve_p({ path: safePath });
96
+ }
97
+ });
98
+
99
+ // Also fire on 'change' so that overwriting an existing file is detected.
100
+ // Without this, upstream tasks that truncate-and-rewrite a file emit only
101
+ // a 'change' event and the downstream trigger would never resolve.
102
+ watcher.on('change', (changedPath: string) => {
103
+ if (settled) return;
104
+ if (pathsEqual(resolve(dir, changedPath), safePath)) {
105
+ cleanup();
106
+ resolve_p({ path: safePath });
107
+ }
108
+ });
109
+
110
+ watcher.on('error', (err: unknown) => {
111
+ if (settled) return;
112
+ cleanup();
113
+ reject(
114
+ new Error(
115
+ `file trigger watch error: ${err instanceof Error ? err.message : String(err)}`,
116
+ ),
117
+ );
118
+ });
119
+
120
+ // After the watcher finishes its initial scan, check if the file already exists.
121
+ // Doing this inside 'ready' eliminates the race window between existence check
122
+ // and watcher startup, so we neither miss events nor double-resolve.
123
+ watcher.on('ready', () => {
124
+ if (settled) return;
125
+ Bun.file(safePath)
126
+ .exists()
127
+ .then((exists) => {
128
+ if (settled) return;
129
+ if (exists) {
130
+ cleanup();
131
+ resolve_p({ path: safePath });
132
+ }
133
+ })
134
+ .catch((err: unknown) => {
135
+ if (settled) return;
136
+ cleanup();
137
+ reject(
138
+ new Error(
139
+ `file trigger existence check failed: ${err instanceof Error ? err.message : String(err)}`,
140
+ ),
141
+ );
142
+ });
143
+ });
144
+
145
+ if (timeoutMs > 0) {
146
+ timer = setTimeout(() => {
147
+ if (settled) return;
148
+ cleanup();
149
+ reject(
150
+ new TriggerTimeoutError(
151
+ `file trigger timeout: ${filePath} did not appear within ${config.timeout}`,
152
+ ),
153
+ );
154
+ }, timeoutMs);
155
+ }
156
+
157
+ ctx.signal.addEventListener('abort', onAbort);
158
+ }
159
+
160
+ return new Promise((resolve_p, reject) => {
161
+ start(resolve_p, reject).catch(reject);
162
+ });
163
+ },
164
+ };