@tagma/sdk 0.4.12 → 0.4.14

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 (88) hide show
  1. package/README.md +569 -566
  2. package/dist/adapters/websocket-approval.d.ts.map +1 -1
  3. package/dist/adapters/websocket-approval.js +3 -1
  4. package/dist/adapters/websocket-approval.js.map +1 -1
  5. package/dist/approval.d.ts.map +1 -1
  6. package/dist/approval.js.map +1 -1
  7. package/dist/completions/exit-code.d.ts.map +1 -1
  8. package/dist/completions/exit-code.js.map +1 -1
  9. package/dist/completions/file-exists.d.ts.map +1 -1
  10. package/dist/completions/file-exists.js.map +1 -1
  11. package/dist/completions/output-check.js +2 -7
  12. package/dist/completions/output-check.js.map +1 -1
  13. package/dist/config-ops.d.ts.map +1 -1
  14. package/dist/config-ops.js +24 -26
  15. package/dist/config-ops.js.map +1 -1
  16. package/dist/dag.d.ts.map +1 -1
  17. package/dist/dag.js +1 -1
  18. package/dist/dag.js.map +1 -1
  19. package/dist/drivers/claude-code.d.ts.map +1 -1
  20. package/dist/drivers/claude-code.js +10 -5
  21. package/dist/drivers/claude-code.js.map +1 -1
  22. package/dist/engine.d.ts.map +1 -1
  23. package/dist/engine.js +54 -27
  24. package/dist/engine.js.map +1 -1
  25. package/dist/hooks.d.ts.map +1 -1
  26. package/dist/hooks.js +1 -3
  27. package/dist/hooks.js.map +1 -1
  28. package/dist/logger.d.ts.map +1 -1
  29. package/dist/logger.js +4 -2
  30. package/dist/logger.js.map +1 -1
  31. package/dist/pipeline-runner.d.ts.map +1 -1
  32. package/dist/pipeline-runner.js +10 -4
  33. package/dist/pipeline-runner.js.map +1 -1
  34. package/dist/registry.d.ts +11 -1
  35. package/dist/registry.d.ts.map +1 -1
  36. package/dist/registry.js +28 -3
  37. package/dist/registry.js.map +1 -1
  38. package/dist/runner.d.ts.map +1 -1
  39. package/dist/runner.js +18 -13
  40. package/dist/runner.js.map +1 -1
  41. package/dist/schema.d.ts.map +1 -1
  42. package/dist/schema.js +14 -14
  43. package/dist/schema.js.map +1 -1
  44. package/dist/schema.test.js +5 -1
  45. package/dist/schema.test.js.map +1 -1
  46. package/dist/sdk.d.ts +2 -2
  47. package/dist/sdk.d.ts.map +1 -1
  48. package/dist/sdk.js +1 -1
  49. package/dist/sdk.js.map +1 -1
  50. package/dist/triggers/file.d.ts.map +1 -1
  51. package/dist/triggers/file.js +11 -4
  52. package/dist/triggers/file.js.map +1 -1
  53. package/dist/triggers/manual.d.ts.map +1 -1
  54. package/dist/triggers/manual.js +2 -1
  55. package/dist/triggers/manual.js.map +1 -1
  56. package/dist/utils.d.ts.map +1 -1
  57. package/dist/utils.js +13 -6
  58. package/dist/utils.js.map +1 -1
  59. package/dist/validate-raw.d.ts.map +1 -1
  60. package/dist/validate-raw.js +40 -11
  61. package/dist/validate-raw.js.map +1 -1
  62. package/package.json +2 -2
  63. package/scripts/preinstall.js +1 -1
  64. package/src/adapters/stdin-approval.ts +106 -106
  65. package/src/adapters/websocket-approval.ts +224 -220
  66. package/src/approval.ts +131 -125
  67. package/src/bootstrap.ts +37 -37
  68. package/src/completions/exit-code.ts +34 -30
  69. package/src/completions/file-exists.ts +66 -60
  70. package/src/completions/output-check.ts +86 -86
  71. package/src/config-ops.ts +307 -322
  72. package/src/dag.ts +234 -228
  73. package/src/drivers/claude-code.ts +250 -240
  74. package/src/engine.ts +1098 -935
  75. package/src/hooks.ts +187 -179
  76. package/src/logger.ts +182 -178
  77. package/src/middlewares/static-context.ts +45 -45
  78. package/src/pipeline-runner.ts +156 -150
  79. package/src/registry.ts +51 -23
  80. package/src/runner.ts +395 -397
  81. package/src/schema.test.ts +5 -1
  82. package/src/schema.ts +338 -328
  83. package/src/sdk.ts +91 -81
  84. package/src/triggers/file.ts +33 -14
  85. package/src/triggers/manual.ts +86 -81
  86. package/src/types.ts +18 -18
  87. package/src/utils.ts +202 -191
  88. package/src/validate-raw.ts +442 -409
@@ -1,150 +1,156 @@
1
- // ═══ PipelineRunner ═══
2
- //
3
- // Wraps runPipeline in a lifecycle object suited for multi-pipeline management
4
- // in sidecar / Tauri IPC scenarios. Each instance controls one pipeline run.
5
- //
6
- // Typical sidecar usage:
7
- //
8
- // const runners = new Map<string, PipelineRunner>();
9
- //
10
- // const runner = new PipelineRunner(config, workDir);
11
- // runner.subscribe(event => ipcEmit('pipeline_event', event));
12
- // runner.start();
13
- // runners.set(runner.instanceId, runner);
14
- //
15
- // // Later, from IPC:
16
- // runners.get(id)?.abort();
17
-
18
- import { runPipeline } from './engine';
19
- import type { EngineResult, PipelineEvent, RunPipelineOptions } from './engine';
20
- import type { PipelineConfig, TaskState } from './types';
21
- import { generateRunId } from './utils';
22
-
23
- export type { PipelineEvent, EngineResult };
24
-
25
- export type PipelineRunnerStatus = 'idle' | 'running' | 'done' | 'aborted';
26
-
27
- export class PipelineRunner {
28
- /**
29
- * Stable ID assigned before start() — safe to use as a Map key in the sidecar
30
- * before the engine-assigned runId becomes available.
31
- */
32
- readonly instanceId: string;
33
-
34
- /**
35
- * The runId generated by the engine. Available after the first 'pipeline_start'
36
- * event fires (i.e. effectively immediately after start() is called).
37
- * null until then.
38
- */
39
- private _runId: string | null = null;
40
- private _status: PipelineRunnerStatus = 'idle';
41
- private _result: Promise<EngineResult> | null = null;
42
- private _abortController = new AbortController();
43
- private _handlers = new Set<(event: PipelineEvent) => void>();
44
- private _states: ReadonlyMap<string, TaskState> | null = null;
45
- private _statesMirror = new Map<string, TaskState>();
46
-
47
- constructor(
48
- private readonly config: PipelineConfig,
49
- private readonly workDir: string,
50
- private readonly opts: Omit<RunPipelineOptions, 'signal' | 'onEvent'> = {},
51
- ) {
52
- this.instanceId = generateRunId();
53
- }
54
-
55
- get runId(): string | null { return this._runId; }
56
- get status(): PipelineRunnerStatus { return this._status; }
57
-
58
- /**
59
- * Start the pipeline. Calling start() more than once returns the same Promise.
60
- */
61
- start(): Promise<EngineResult> {
62
- if (this._result) return this._result;
63
-
64
- // Guard: if abort() was called before start(), the signal is already
65
- // aborted. Create a fresh controller so the pipeline doesn't terminate
66
- // immediately. If users truly want pre-abort semantics, they call
67
- // abort() after start().
68
- if (this._abortController.signal.aborted) {
69
- this._abortController = new AbortController();
70
- this._status = 'idle';
71
- }
72
-
73
- this._status = 'running';
74
- this._result = runPipeline(this.config, this.workDir, {
75
- ...this.opts,
76
- signal: this._abortController.signal,
77
- onEvent: (event) => {
78
- if (event.type === 'pipeline_start') {
79
- this._runId = event.runId;
80
- // Initialize the live mirror with the full initial state snapshot
81
- for (const [id, state] of event.states) {
82
- this._statesMirror.set(id, { ...state });
83
- }
84
- }
85
- if (event.type === 'task_status_change') {
86
- // Keep the mirror up to date so getStates() works during the run
87
- this._statesMirror.set(event.taskId, event.state);
88
- }
89
- if (event.type === 'pipeline_end') {
90
- this._status = this._abortController.signal.aborted ? 'aborted' : 'done';
91
- }
92
- for (const h of this._handlers) h(event);
93
- },
94
- }).then(result => {
95
- this._states = result.states;
96
- if (this._status === 'running') this._status = 'done';
97
- return result;
98
- }).catch(err => {
99
- this._status = 'aborted';
100
- throw err;
101
- });
102
-
103
- return this._result;
104
- }
105
-
106
- /**
107
- * Cancel the running pipeline. Safe to call multiple times or before start().
108
- */
109
- abort(reason?: string): void {
110
- this._status = 'aborted';
111
- this._abortController.abort(reason);
112
- }
113
-
114
- /**
115
- * Live snapshot of task states. Available from the first pipeline_start event onward
116
- * (i.e. as soon as start() is called) and remains accessible after the run completes.
117
- * Returns null only if the pipeline has never started.
118
- */
119
- getStates(): ReadonlyMap<string, TaskState> | null {
120
- if (this._states) return snapshotStates(this._states);
121
- if (this._statesMirror.size > 0) return snapshotStates(this._statesMirror);
122
- return null;
123
- }
124
-
125
- /**
126
- * Subscribe to pipeline/task events. Returns an unsubscribe function.
127
- * Events are emitted synchronously in the engine's event loop, so keep
128
- * handlers non-blocking (e.g. queue to IPC, do not await inside).
129
- */
130
- subscribe(handler: (event: PipelineEvent) => void): () => void {
131
- this._handlers.add(handler);
132
- return () => this._handlers.delete(handler);
133
- }
134
- }
135
-
136
- /** Deep-copy a states map so callers cannot mutate SDK internals. */
137
- function snapshotStates(src: ReadonlyMap<string, TaskState>): ReadonlyMap<string, TaskState> {
138
- const copy = new Map<string, TaskState>();
139
- for (const [id, s] of src) {
140
- copy.set(id, {
141
- config: { ...s.config },
142
- trackConfig: { ...s.trackConfig },
143
- status: s.status,
144
- result: s.result ? { ...s.result } : null,
145
- startedAt: s.startedAt,
146
- finishedAt: s.finishedAt,
147
- });
148
- }
149
- return copy;
150
- }
1
+ // ═══ PipelineRunner ═══
2
+ //
3
+ // Wraps runPipeline in a lifecycle object suited for multi-pipeline management
4
+ // in sidecar / Tauri IPC scenarios. Each instance controls one pipeline run.
5
+ //
6
+ // Typical sidecar usage:
7
+ //
8
+ // const runners = new Map<string, PipelineRunner>();
9
+ //
10
+ // const runner = new PipelineRunner(config, workDir);
11
+ // runner.subscribe(event => ipcEmit('pipeline_event', event));
12
+ // runner.start();
13
+ // runners.set(runner.instanceId, runner);
14
+ //
15
+ // // Later, from IPC:
16
+ // runners.get(id)?.abort();
17
+
18
+ import { runPipeline } from './engine';
19
+ import type { EngineResult, PipelineEvent, RunPipelineOptions } from './engine';
20
+ import type { PipelineConfig, TaskState } from './types';
21
+ import { generateRunId } from './utils';
22
+
23
+ export type { PipelineEvent, EngineResult };
24
+
25
+ export type PipelineRunnerStatus = 'idle' | 'running' | 'done' | 'aborted';
26
+
27
+ export class PipelineRunner {
28
+ /**
29
+ * Stable ID assigned before start() — safe to use as a Map key in the sidecar
30
+ * before the engine-assigned runId becomes available.
31
+ */
32
+ readonly instanceId: string;
33
+
34
+ /**
35
+ * The runId generated by the engine. Available after the first 'pipeline_start'
36
+ * event fires (i.e. effectively immediately after start() is called).
37
+ * null until then.
38
+ */
39
+ private _runId: string | null = null;
40
+ private _status: PipelineRunnerStatus = 'idle';
41
+ private _result: Promise<EngineResult> | null = null;
42
+ private _abortController = new AbortController();
43
+ private _handlers = new Set<(event: PipelineEvent) => void>();
44
+ private _states: ReadonlyMap<string, TaskState> | null = null;
45
+ private _statesMirror = new Map<string, TaskState>();
46
+
47
+ constructor(
48
+ private readonly config: PipelineConfig,
49
+ private readonly workDir: string,
50
+ private readonly opts: Omit<RunPipelineOptions, 'signal' | 'onEvent'> = {},
51
+ ) {
52
+ this.instanceId = generateRunId();
53
+ }
54
+
55
+ get runId(): string | null {
56
+ return this._runId;
57
+ }
58
+ get status(): PipelineRunnerStatus {
59
+ return this._status;
60
+ }
61
+
62
+ /**
63
+ * Start the pipeline. Calling start() more than once returns the same Promise.
64
+ */
65
+ start(): Promise<EngineResult> {
66
+ if (this._result) return this._result;
67
+
68
+ // Guard: if abort() was called before start(), the signal is already
69
+ // aborted. Create a fresh controller so the pipeline doesn't terminate
70
+ // immediately. If users truly want pre-abort semantics, they call
71
+ // abort() after start().
72
+ if (this._abortController.signal.aborted) {
73
+ this._abortController = new AbortController();
74
+ this._status = 'idle';
75
+ }
76
+
77
+ this._status = 'running';
78
+ this._result = runPipeline(this.config, this.workDir, {
79
+ ...this.opts,
80
+ signal: this._abortController.signal,
81
+ onEvent: (event) => {
82
+ if (event.type === 'pipeline_start') {
83
+ this._runId = event.runId;
84
+ // Initialize the live mirror with the full initial state snapshot
85
+ for (const [id, state] of event.states) {
86
+ this._statesMirror.set(id, { ...state });
87
+ }
88
+ }
89
+ if (event.type === 'task_status_change') {
90
+ // Keep the mirror up to date so getStates() works during the run
91
+ this._statesMirror.set(event.taskId, event.state);
92
+ }
93
+ if (event.type === 'pipeline_end') {
94
+ this._status = this._abortController.signal.aborted ? 'aborted' : 'done';
95
+ }
96
+ for (const h of this._handlers) h(event);
97
+ },
98
+ })
99
+ .then((result) => {
100
+ this._states = result.states;
101
+ if (this._status === 'running') this._status = 'done';
102
+ return result;
103
+ })
104
+ .catch((err) => {
105
+ this._status = 'aborted';
106
+ throw err;
107
+ });
108
+
109
+ return this._result;
110
+ }
111
+
112
+ /**
113
+ * Cancel the running pipeline. Safe to call multiple times or before start().
114
+ */
115
+ abort(reason?: string): void {
116
+ this._status = 'aborted';
117
+ this._abortController.abort(reason);
118
+ }
119
+
120
+ /**
121
+ * Live snapshot of task states. Available from the first pipeline_start event onward
122
+ * (i.e. as soon as start() is called) and remains accessible after the run completes.
123
+ * Returns null only if the pipeline has never started.
124
+ */
125
+ getStates(): ReadonlyMap<string, TaskState> | null {
126
+ if (this._states) return snapshotStates(this._states);
127
+ if (this._statesMirror.size > 0) return snapshotStates(this._statesMirror);
128
+ return null;
129
+ }
130
+
131
+ /**
132
+ * Subscribe to pipeline/task events. Returns an unsubscribe function.
133
+ * Events are emitted synchronously in the engine's event loop, so keep
134
+ * handlers non-blocking (e.g. queue to IPC, do not await inside).
135
+ */
136
+ subscribe(handler: (event: PipelineEvent) => void): () => void {
137
+ this._handlers.add(handler);
138
+ return () => this._handlers.delete(handler);
139
+ }
140
+ }
141
+
142
+ /** Deep-copy a states map so callers cannot mutate SDK internals. */
143
+ function snapshotStates(src: ReadonlyMap<string, TaskState>): ReadonlyMap<string, TaskState> {
144
+ const copy = new Map<string, TaskState>();
145
+ for (const [id, s] of src) {
146
+ copy.set(id, {
147
+ config: { ...s.config },
148
+ trackConfig: { ...s.trackConfig },
149
+ status: s.status,
150
+ result: s.result ? { ...s.result } : null,
151
+ startedAt: s.startedAt,
152
+ finishedAt: s.finishedAt,
153
+ });
154
+ }
155
+ return copy;
156
+ }
package/src/registry.ts CHANGED
@@ -1,17 +1,26 @@
1
+ import { createRequire } from 'node:module';
2
+ import { pathToFileURL } from 'node:url';
1
3
  import type {
2
- PluginCategory, DriverPlugin, TriggerPlugin,
3
- CompletionPlugin, MiddlewarePlugin, PluginManifest,
4
+ PluginCategory,
5
+ DriverPlugin,
6
+ TriggerPlugin,
7
+ CompletionPlugin,
8
+ MiddlewarePlugin,
9
+ PluginManifest,
4
10
  } from './types';
5
11
 
6
12
  type PluginType = DriverPlugin | TriggerPlugin | CompletionPlugin | MiddlewarePlugin;
7
13
 
8
14
  const VALID_CATEGORIES: ReadonlySet<PluginCategory> = new Set([
9
- 'drivers', 'triggers', 'completions', 'middlewares',
15
+ 'drivers',
16
+ 'triggers',
17
+ 'completions',
18
+ 'middlewares',
10
19
  ]);
11
20
 
12
21
  const registries = {
13
- drivers: new Map<string, DriverPlugin>(),
14
- triggers: new Map<string, TriggerPlugin>(),
22
+ drivers: new Map<string, DriverPlugin>(),
23
+ triggers: new Map<string, TriggerPlugin>(),
15
24
  completions: new Map<string, CompletionPlugin>(),
16
25
  middlewares: new Map<string, MiddlewarePlugin>(),
17
26
  };
@@ -47,7 +56,7 @@ function validateContract(category: PluginCategory, handler: unknown): void {
47
56
  } catch (err) {
48
57
  throw new Error(
49
58
  `drivers plugin "${h.name}" capabilities accessor threw: ` +
50
- (err instanceof Error ? err.message : String(err))
59
+ (err instanceof Error ? err.message : String(err)),
51
60
  );
52
61
  }
53
62
  if (!caps || typeof caps !== 'object') {
@@ -57,16 +66,14 @@ function validateContract(category: PluginCategory, handler: unknown): void {
57
66
  for (const field of ['sessionResume', 'systemPrompt', 'outputFormat'] as const) {
58
67
  if (typeof c[field] !== 'boolean') {
59
68
  throw new Error(
60
- `drivers plugin "${h.name}".capabilities.${field} must be a boolean (got ${typeof c[field]})`
69
+ `drivers plugin "${h.name}".capabilities.${field} must be a boolean (got ${typeof c[field]})`,
61
70
  );
62
71
  }
63
72
  }
64
73
  // Optional methods, but if present must be functions.
65
74
  for (const opt of ['parseResult', 'resolveModel', 'resolveTools'] as const) {
66
75
  if (h[opt] !== undefined && typeof h[opt] !== 'function') {
67
- throw new Error(
68
- `drivers plugin "${h.name}".${opt} must be a function or undefined`
69
- );
76
+ throw new Error(`drivers plugin "${h.name}".${opt} must be a function or undefined`);
70
77
  }
71
78
  }
72
79
  break;
@@ -101,7 +108,9 @@ export type RegisterResult = 'registered' | 'replaced' | 'unchanged';
101
108
  * minimum interface contract for the category.
102
109
  */
103
110
  export function registerPlugin<T extends PluginType>(
104
- category: PluginCategory, type: string, handler: T,
111
+ category: PluginCategory,
112
+ type: string,
113
+ handler: T,
105
114
  ): RegisterResult {
106
115
  if (!VALID_CATEGORIES.has(category)) {
107
116
  throw new Error(`Unknown plugin category "${category}"`);
@@ -129,14 +138,12 @@ export function unregisterPlugin(category: PluginCategory, type: string): boolea
129
138
  return registries[category].delete(type);
130
139
  }
131
140
 
132
- export function getHandler<T extends PluginType>(
133
- category: PluginCategory, type: string,
134
- ): T {
141
+ export function getHandler<T extends PluginType>(category: PluginCategory, type: string): T {
135
142
  const handler = registries[category].get(type);
136
143
  if (!handler) {
137
144
  throw new Error(
138
145
  `${category} type "${type}" not registered.\n` +
139
- `Install the plugin: bun add @tagma/${category.replace(/s$/, '')}-${type}`
146
+ `Install the plugin: bun add @tagma/${category.replace(/s$/, '')}-${type}`,
140
147
  );
141
148
  }
142
149
  return handler as T;
@@ -181,7 +188,7 @@ export function readPluginManifest(pkgJson: unknown): PluginManifest | null {
181
188
  const type = m.type;
182
189
  if (typeof category !== 'string' || !VALID_CATEGORIES.has(category as PluginCategory)) {
183
190
  throw new Error(
184
- `tagmaPlugin.category must be one of ${[...VALID_CATEGORIES].join(', ')}, got ${JSON.stringify(category)}`
191
+ `tagmaPlugin.category must be one of ${[...VALID_CATEGORIES].join(', ')}, got ${JSON.stringify(category)}`,
185
192
  );
186
193
  }
187
194
  if (typeof type !== 'string' || type.length === 0) {
@@ -190,20 +197,41 @@ export function readPluginManifest(pkgJson: unknown): PluginManifest | null {
190
197
  return { category: category as PluginCategory, type };
191
198
  }
192
199
 
193
- export async function loadPlugins(pluginNames: readonly string[]): Promise<void> {
200
+ /**
201
+ * Load and register a list of plugin packages.
202
+ *
203
+ * @param pluginNames - Validated npm package names to load.
204
+ * @param resolveFrom - Optional absolute path to resolve plugins from (e.g. the
205
+ * workspace's working directory). When omitted, the default ESM resolution
206
+ * uses the SDK's own `node_modules`, which will fail for plugins installed
207
+ * only in the user's workspace. CLI callers should pass `process.cwd()` or
208
+ * the workspace root so that workspace-local plugins resolve correctly.
209
+ */
210
+ export async function loadPlugins(
211
+ pluginNames: readonly string[],
212
+ resolveFrom?: string,
213
+ ): Promise<void> {
194
214
  for (const name of pluginNames) {
195
215
  if (!isValidPluginName(name)) {
196
216
  throw new Error(
197
217
  `Plugin "${name}" rejected: plugin names must be scoped npm packages ` +
198
- `(e.g. @tagma/trigger-xyz) or tagma-plugin-* packages. ` +
199
- `Relative/absolute paths are not allowed.`
218
+ `(e.g. @tagma/trigger-xyz) or tagma-plugin-* packages. ` +
219
+ `Relative/absolute paths are not allowed.`,
200
220
  );
201
221
  }
202
- const mod = await import(name);
222
+ let moduleUrl: string = name;
223
+ if (resolveFrom) {
224
+ // Resolve the package entry point relative to the caller's directory so
225
+ // plugins installed in the workspace's node_modules are found even when
226
+ // the SDK itself lives elsewhere (e.g. a global install or a monorepo
227
+ // sibling package).
228
+ const req = createRequire(resolveFrom.endsWith('/') ? resolveFrom : resolveFrom + '/');
229
+ const resolved = req.resolve(name);
230
+ moduleUrl = pathToFileURL(resolved).href;
231
+ }
232
+ const mod = await import(moduleUrl);
203
233
  if (!mod.pluginCategory || !mod.pluginType || !mod.default) {
204
- throw new Error(
205
- `Plugin "${name}" must export pluginCategory, pluginType, and default`
206
- );
234
+ throw new Error(`Plugin "${name}" must export pluginCategory, pluginType, and default`);
207
235
  }
208
236
  registerPlugin(mod.pluginCategory, mod.pluginType, mod.default);
209
237
  }