agent-tempo 1.1.0 → 1.3.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.
Files changed (103) hide show
  1. package/CLAUDE.md +219 -219
  2. package/LICENSE +21 -21
  3. package/README.md +289 -289
  4. package/assets/icon-dark.svg +9 -9
  5. package/assets/icon.svg +9 -9
  6. package/assets/logo-dark.svg +11 -11
  7. package/assets/logo-light.svg +11 -11
  8. package/dashboard/README.md +91 -91
  9. package/dashboard/dist/assets/index-D6Xyje_n.js.map +1 -1
  10. package/dashboard/dist/index.html +19 -19
  11. package/dashboard/package.json +47 -47
  12. package/dist/adapters/copilot/adapter.js +12 -1
  13. package/dist/cli/commands.d.ts +39 -0
  14. package/dist/cli/commands.js +83 -2
  15. package/dist/cli/global-wrapper.d.ts +19 -0
  16. package/dist/cli/global-wrapper.js +169 -0
  17. package/dist/cli/help-text.js +97 -97
  18. package/dist/cli/sa-preflight.d.ts +27 -3
  19. package/dist/cli/sa-preflight.js +169 -9
  20. package/dist/cli/startup.js +45 -8
  21. package/dist/cli/upgrade-command.js +81 -81
  22. package/dist/cli.js +12 -0
  23. package/dist/daemon.js +6 -0
  24. package/dist/http/catalog.js +17 -3
  25. package/dist/scripts/verify-daemon-isolation-guard.js +24 -24
  26. package/dist/server.js +4 -0
  27. package/dist/spawn.js +12 -12
  28. package/dist/tools/coat-check-evict.js +2 -2
  29. package/dist/tools/coat-check-get.js +2 -2
  30. package/dist/tools/coat-check-put.js +4 -4
  31. package/dist/tools/fetch-state.js +2 -2
  32. package/dist/tools/save-state.js +13 -13
  33. package/dist/utils/grpc-shutdown-guard.d.ts +52 -0
  34. package/dist/utils/grpc-shutdown-guard.js +88 -0
  35. package/examples/agents/tempo-composer.md +56 -56
  36. package/examples/agents/tempo-conductor.md +117 -117
  37. package/examples/agents/tempo-critic.md +73 -73
  38. package/examples/agents/tempo-improv.md +74 -74
  39. package/examples/agents/tempo-liner.md +75 -75
  40. package/examples/agents/tempo-roadie.md +61 -61
  41. package/examples/agents/tempo-soloist.md +71 -71
  42. package/examples/agents/tempo-tuner.md +94 -94
  43. package/examples/ensembles/tempo-big-band.yaml +146 -146
  44. package/examples/ensembles/tempo-dev-team.yaml +58 -58
  45. package/examples/ensembles/tempo-headless-jam.yaml +77 -77
  46. package/examples/ensembles/tempo-jam-session.yaml +41 -41
  47. package/examples/ensembles/tempo-mock-jam.yaml +79 -79
  48. package/examples/ensembles/tempo-review-squad.yaml +32 -32
  49. package/package.json +173 -172
  50. package/packaging/launchd/com.agent.tempo.plist +46 -46
  51. package/packaging/systemd/agent-tempo.service +32 -32
  52. package/packaging/windows/install-task.ps1 +71 -71
  53. package/scenarios/conductor-recruit-mock.yaml +33 -33
  54. package/scenarios/echo-roundtrip.yaml +15 -15
  55. package/scenarios/multi-player-handoff.yaml +38 -38
  56. package/scenarios/recruit-cascade.yaml +38 -38
  57. package/scenarios/two-player-conversation.yaml +33 -33
  58. package/workflow-bundle.js +1 -1
  59. package/dist/activities/claude-stop.d.ts +0 -21
  60. package/dist/activities/claude-stop.js +0 -94
  61. package/dist/channel.d.ts +0 -3
  62. package/dist/channel.js +0 -48
  63. package/dist/copilot-bridge.d.ts +0 -22
  64. package/dist/copilot-bridge.js +0 -565
  65. package/dist/scripts/258-spotcheck.js +0 -303
  66. package/dist/tools/detach.d.ts +0 -4
  67. package/dist/tools/detach.js +0 -45
  68. package/dist/tools/encore.d.ts +0 -4
  69. package/dist/tools/encore.js +0 -31
  70. package/dist/tools/pause-ensemble.d.ts +0 -4
  71. package/dist/tools/pause-ensemble.js +0 -58
  72. package/dist/tools/resume-ensemble.d.ts +0 -4
  73. package/dist/tools/resume-ensemble.js +0 -79
  74. package/dist/tools/stop.d.ts +0 -4
  75. package/dist/tools/stop.js +0 -29
  76. package/dist/tui/client.d.ts +0 -6
  77. package/dist/tui/client.js +0 -9
  78. package/dist/tui/components/ActivityLog.d.ts +0 -16
  79. package/dist/tui/components/ActivityLog.js +0 -36
  80. package/dist/tui/components/CommandOverlay.d.ts +0 -15
  81. package/dist/tui/components/CommandOverlay.js +0 -34
  82. package/dist/tui/components/ConductorChat.d.ts +0 -16
  83. package/dist/tui/components/ConductorChat.js +0 -32
  84. package/dist/tui/components/EnsembleListView.d.ts +0 -14
  85. package/dist/tui/components/EnsembleListView.js +0 -32
  86. package/dist/tui/components/EnsemblePanel.d.ts +0 -12
  87. package/dist/tui/components/EnsemblePanel.js +0 -40
  88. package/dist/tui/components/InputBar.d.ts +0 -13
  89. package/dist/tui/components/InputBar.js +0 -58
  90. package/dist/tui/components/ScheduleOverlay.d.ts +0 -13
  91. package/dist/tui/components/ScheduleOverlay.js +0 -113
  92. package/dist/tui/components/TopBar.d.ts +0 -12
  93. package/dist/tui/components/TopBar.js +0 -15
  94. package/dist/tui/core-api.d.ts +0 -26
  95. package/dist/tui/core-api.js +0 -67
  96. package/dist/tui/hooks/useEnsembleDiscovery.d.ts +0 -3
  97. package/dist/tui/hooks/useEnsembleDiscovery.js +0 -30
  98. package/dist/tui/hooks/useMaestroPoller.d.ts +0 -3
  99. package/dist/tui/hooks/useMaestroPoller.js +0 -36
  100. package/dist/tui/hooks/useSendCommand.d.ts +0 -7
  101. package/dist/tui/hooks/useSendCommand.js +0 -29
  102. package/dist/utils/bg-preflight.d.ts +0 -25
  103. package/dist/utils/bg-preflight.js +0 -154
@@ -1,21 +1,21 @@
1
- <!doctype html>
2
- <html lang="en" data-theme="dark" data-density="6" data-accent="terracotta">
3
- <head>
4
- <meta charset="UTF-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
- <meta name="color-scheme" content="dark light" />
7
- <meta name="description" content="agent-tempo Maestro Dashboard" />
8
- <title>agent-tempo · Maestro</title>
9
- <link rel="preconnect" href="https://fonts.googleapis.com" />
10
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
11
- <link
12
- rel="stylesheet"
13
- href="https://fonts.googleapis.com/css2?family=Instrument+Sans:wght@400;500;600;700&family=Instrument+Serif:ital@0;1&family=JetBrains+Mono:wght@400;500&display=swap"
14
- />
1
+ <!doctype html>
2
+ <html lang="en" data-theme="dark" data-density="6" data-accent="terracotta">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <meta name="color-scheme" content="dark light" />
7
+ <meta name="description" content="agent-tempo Maestro Dashboard" />
8
+ <title>agent-tempo · Maestro</title>
9
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
10
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
11
+ <link
12
+ rel="stylesheet"
13
+ href="https://fonts.googleapis.com/css2?family=Instrument+Sans:wght@400;500;600;700&family=Instrument+Serif:ital@0;1&family=JetBrains+Mono:wght@400;500&display=swap"
14
+ />
15
15
  <script type="module" crossorigin src="/dashboard/assets/index-D6Xyje_n.js"></script>
16
16
  <link rel="stylesheet" crossorigin href="/dashboard/assets/index-CB78ToNE.css">
17
- </head>
18
- <body>
19
- <div id="root"></div>
20
- </body>
21
- </html>
17
+ </head>
18
+ <body>
19
+ <div id="root"></div>
20
+ </body>
21
+ </html>
@@ -1,47 +1,47 @@
1
- {
2
- "name": "agent-tempo-dashboard",
3
- "private": true,
4
- "version": "1.1.0",
5
- "type": "module",
6
- "description": "Web dashboard for agent-tempo. Bundled into the npm package; served by the daemon at /dashboard/*.",
7
- "scripts": {
8
- "dev": "vite",
9
- "build": "tsc -b && vite build",
10
- "build:overflow": "tsc -b && vite build --mode overflow",
11
- "preview": "vite preview",
12
- "lint": "eslint src/ tests/",
13
- "test": "vitest run",
14
- "test:watch": "vitest",
15
- "test:e2e": "playwright test",
16
- "test:e2e:install": "playwright install chromium",
17
- "test:overflow": "playwright test --config tests-overflow/playwright.config.ts"
18
- },
19
- "dependencies": {
20
- "@radix-ui/react-dialog": "~1.1.15",
21
- "@tanstack/react-query": "5.100.5",
22
- "react": "19.2.5",
23
- "react-dom": "19.2.5",
24
- "react-router-dom": "7.14.2",
25
- "zustand": "^5.0.0"
26
- },
27
- "devDependencies": {
28
- "@eslint/js": "^9.0.0",
29
- "@playwright/test": "^1.50.0",
30
- "@tailwindcss/vite": "4.2.4",
31
- "@testing-library/dom": "^10.0.0",
32
- "@testing-library/jest-dom": "^6.6.0",
33
- "@testing-library/react": "^16.1.0",
34
- "@types/node": "^22.0.0",
35
- "@types/react": "^19.2.0",
36
- "@types/react-dom": "^19.2.0",
37
- "@typescript-eslint/parser": "^8.0.0",
38
- "@vitejs/plugin-react": "^5.0.0",
39
- "eslint": "^9.0.0",
40
- "eslint-plugin-react-hooks": "^5.1.0",
41
- "jsdom": "^25.0.0",
42
- "tailwindcss": "4.2.4",
43
- "typescript": "^5.7.0",
44
- "vite": "8.0.10",
45
- "vitest": "^2.1.9"
46
- }
47
- }
1
+ {
2
+ "name": "agent-tempo-dashboard",
3
+ "private": true,
4
+ "version": "1.3.1",
5
+ "type": "module",
6
+ "description": "Web dashboard for agent-tempo. Bundled into the npm package; served by the daemon at /dashboard/*.",
7
+ "scripts": {
8
+ "dev": "vite",
9
+ "build": "tsc -b && vite build",
10
+ "build:overflow": "tsc -b && vite build --mode overflow",
11
+ "preview": "vite preview",
12
+ "lint": "eslint src/ tests/",
13
+ "test": "vitest run",
14
+ "test:watch": "vitest",
15
+ "test:e2e": "playwright test",
16
+ "test:e2e:install": "playwright install chromium",
17
+ "test:overflow": "playwright test --config tests-overflow/playwright.config.ts"
18
+ },
19
+ "dependencies": {
20
+ "@radix-ui/react-dialog": "~1.1.15",
21
+ "@tanstack/react-query": "5.100.5",
22
+ "react": "19.2.5",
23
+ "react-dom": "19.2.5",
24
+ "react-router-dom": "7.14.2",
25
+ "zustand": "^5.0.0"
26
+ },
27
+ "devDependencies": {
28
+ "@eslint/js": "^9.0.0",
29
+ "@playwright/test": "^1.50.0",
30
+ "@tailwindcss/vite": "4.2.4",
31
+ "@testing-library/dom": "^10.0.0",
32
+ "@testing-library/jest-dom": "^6.6.0",
33
+ "@testing-library/react": "^16.1.0",
34
+ "@types/node": "^22.0.0",
35
+ "@types/react": "^19.2.0",
36
+ "@types/react-dom": "^19.2.0",
37
+ "@typescript-eslint/parser": "^8.0.0",
38
+ "@vitejs/plugin-react": "^5.0.0",
39
+ "eslint": "^9.0.0",
40
+ "eslint-plugin-react-hooks": "^5.1.0",
41
+ "jsdom": "^25.0.0",
42
+ "tailwindcss": "4.2.4",
43
+ "typescript": "^5.7.0",
44
+ "vite": "8.0.10",
45
+ "vitest": "^2.1.9"
46
+ }
47
+ }
@@ -274,6 +274,7 @@ class CopilotSdkAttachment extends base_1.SdkAttachment {
274
274
  // Spawn Copilot SDK client and session
275
275
  const copilotClient = new CopilotClient({
276
276
  logLevel: 'debug',
277
+ workingDirectory: workDir,
277
278
  env: {
278
279
  ...cleanEnv(),
279
280
  ...(process.env.GITHUB_TOKEN ? { GITHUB_TOKEN: process.env.GITHUB_TOKEN } : {}),
@@ -286,7 +287,6 @@ class CopilotSdkAttachment extends base_1.SdkAttachment {
286
287
  // All tool calls are auto-approved by design — the bridge operator accepts
287
288
  // this when launching the bridge process.
288
289
  onPermissionRequest: approveAll,
289
- workingDirectory: workDir,
290
290
  mcpServers: {
291
291
  'agent-tempo': {
292
292
  command: serverCommand,
@@ -302,6 +302,17 @@ class CopilotSdkAttachment extends base_1.SdkAttachment {
302
302
  // `--append-system-prompt` argv. Behavior here is unchanged.
303
303
  content: (0, system_prompt_1.buildSdkSystemPrompt)({ ensemble: config.ensemble }),
304
304
  },
305
+ hooks: {
306
+ // Auto-allow agent-tempo MCP tools to skip the permission prompt round-trip.
307
+ // This eliminates the permission.requested → handler → approval cycle for
308
+ // every MCP tool call, reducing latency.
309
+ onPreToolUse: async (input) => {
310
+ if (input.toolName?.startsWith('mcp__agent-tempo__')) {
311
+ return { permissionDecision: 'allow' };
312
+ }
313
+ return undefined;
314
+ },
315
+ },
305
316
  excludedTools: ['write_powershell', 'read_powershell', 'list_powershell'],
306
317
  ...(model ? { model } : {}),
307
318
  };
@@ -132,6 +132,45 @@ export type StopTemporalResult = {
132
132
  * profile collateral damage.
133
133
  */
134
134
  export declare function stopTemporalServer(opts: StopTemporalServerOpts): StopTemporalResult;
135
+ /**
136
+ * Minimal child handle {@link startTemporalForDestroy} needs — `ChildProcess`
137
+ * satisfies it. Kept narrow so unit tests can inject a fake without spawning.
138
+ *
139
+ * @internal
140
+ */
141
+ export interface SpawnedTemporalChild {
142
+ kill(): void;
143
+ unref(): void;
144
+ }
145
+ /**
146
+ * Dependency seam for {@link startTemporalForDestroy} — production callers
147
+ * pass nothing and get the real spawn + reachability probe. Tests inject
148
+ * stubs plus a tiny `pollDelayMs` so the readiness loop runs instantly.
149
+ *
150
+ * @internal
151
+ */
152
+ export interface StartTemporalForDestroyDeps {
153
+ /** Readiness probe — defaults to {@link isTemporalReachable} for `config`. */
154
+ isReachable?: () => Promise<boolean>;
155
+ /** Spawn hook — defaults to a detached `temporal server start-dev`. */
156
+ spawn?: () => SpawnedTemporalChild;
157
+ /** Readiness poll attempts. Default 20. */
158
+ attempts?: number;
159
+ /** Delay between readiness polls, ms. Default 500 (→ 20×500ms = 10s). */
160
+ pollDelayMs?: number;
161
+ }
162
+ /**
163
+ * Start a temporary Temporal dev server just long enough for `down --destroy`
164
+ * to terminate workflows when Temporal happened to be down. Polls for
165
+ * readiness; on timeout it kills the child it spawned so `down` never leaves
166
+ * a stray Temporal process booting in the background. Exported for unit
167
+ * tests — production callers pass only `config`.
168
+ *
169
+ * @internal
170
+ */
171
+ export declare function startTemporalForDestroy(config: Config, deps?: StartTemporalForDestroyDeps): Promise<{
172
+ started: boolean;
173
+ }>;
135
174
  export declare function down(opts: DownOpts): Promise<void>;
136
175
  interface AgentTypesCommandOpts {
137
176
  subcommand?: string;
@@ -40,6 +40,7 @@ exports.up = up;
40
40
  exports.formatScheduleRecurrence = formatScheduleRecurrence;
41
41
  exports.lineupScheduleToEntry = lineupScheduleToEntry;
42
42
  exports.stopTemporalServer = stopTemporalServer;
43
+ exports.startTemporalForDestroy = startTemporalForDestroy;
43
44
  exports.down = down;
44
45
  exports.agentTypesCommand = agentTypesCommand;
45
46
  exports.broadcast = broadcast;
@@ -1547,6 +1548,44 @@ function stopTemporalServer(opts) {
1547
1548
  return { action: 'failed', error: err };
1548
1549
  }
1549
1550
  }
1551
+ /**
1552
+ * Start a temporary Temporal dev server just long enough for `down --destroy`
1553
+ * to terminate workflows when Temporal happened to be down. Polls for
1554
+ * readiness; on timeout it kills the child it spawned so `down` never leaves
1555
+ * a stray Temporal process booting in the background. Exported for unit
1556
+ * tests — production callers pass only `config`.
1557
+ *
1558
+ * @internal
1559
+ */
1560
+ async function startTemporalForDestroy(config, deps = {}) {
1561
+ const attempts = deps.attempts ?? 20;
1562
+ const pollDelayMs = deps.pollDelayMs ?? 500;
1563
+ const isReachable = deps.isReachable ?? (() => isTemporalReachable(config));
1564
+ const spawn = deps.spawn ?? (() => {
1565
+ (0, fs_1.mkdirSync)(config_1.AGENT_TEMPO_HOME, { recursive: true });
1566
+ const port = config.temporalAddress.split(':')[1] || '7233';
1567
+ return (0, child_process_1.spawn)('temporal', [
1568
+ 'server', 'start-dev',
1569
+ '--port', port,
1570
+ '--db-filename', DEFAULT_DB_PATH,
1571
+ ], { detached: true, stdio: 'ignore' });
1572
+ });
1573
+ const child = spawn();
1574
+ child.unref();
1575
+ for (let i = 0; i < attempts; i++) {
1576
+ await new Promise(r => setTimeout(r, pollDelayMs));
1577
+ if (await isReachable())
1578
+ return { started: true };
1579
+ }
1580
+ // Timed out. The detached child may still be booting and would come up
1581
+ // orphaned moments after we give up — kill the process we spawned so
1582
+ // `down` doesn't leave a stray Temporal server behind.
1583
+ try {
1584
+ child.kill();
1585
+ }
1586
+ catch { /* already exited */ }
1587
+ return { started: false };
1588
+ }
1550
1589
  async function down(opts) {
1551
1590
  const config = (0, config_1.getConfig)(opts);
1552
1591
  out.heading('agent-tempo teardown');
@@ -1555,7 +1594,35 @@ async function down(opts) {
1555
1594
  : ` Stopping daemon + Temporal. Workflows stay parked for the next ${out.dim('agent-tempo up')}.`);
1556
1595
  // Step 1 (destroy mode only): enumerate + terminate workflows across every
1557
1596
  // ensemble, after a typed confirmation showing the user what's at stake.
1558
- const temporalUp = await isTemporalReachable(config);
1597
+ let temporalUp = await isTemporalReachable(config);
1598
+ // `--destroy` can only terminate workflows while Temporal is reachable.
1599
+ // Workflow state lives durably on disk in ~/.agent-tempo/, so if Temporal
1600
+ // happens to be down when the user runs `down --destroy`, skipping the
1601
+ // destroy step here silently leaves every workflow to be resurrected the
1602
+ // next time anything starts the daemon (an `up`, a `status`, or the TUI).
1603
+ // To make `--destroy` actually mean it, start Temporal temporarily just
1604
+ // long enough to run the terminations — Step 4 below stops it again.
1605
+ let startedTemporalForDestroy = false;
1606
+ if (opts.destroy && !temporalUp) {
1607
+ if (!temporalCliExists()) {
1608
+ out.warn('temporal CLI not found — cannot destroy workflows; they will persist on disk.');
1609
+ }
1610
+ else {
1611
+ out.log(` ${out.dim('...')} Temporal is down — starting it temporarily to destroy workflows...`);
1612
+ const { started } = await startTemporalForDestroy(config);
1613
+ if (started) {
1614
+ temporalUp = true;
1615
+ startedTemporalForDestroy = true;
1616
+ out.success('Temporal started for cleanup');
1617
+ }
1618
+ else {
1619
+ out.warn('Could not start Temporal within 10s — workflows may survive teardown. ' +
1620
+ 'Re-run `agent-tempo down --destroy` once Temporal is up. ' +
1621
+ 'A stray Temporal process may have been left starting — check with ' +
1622
+ '`agent-tempo status` and stop it manually if one is still running.');
1623
+ }
1624
+ }
1625
+ }
1559
1626
  if (opts.destroy && temporalUp) {
1560
1627
  try {
1561
1628
  const connection = await (0, connection_1.createTemporalConnection)(config);
@@ -1625,6 +1692,15 @@ async function down(opts) {
1625
1692
  const confirmed = await typedConfirmPrompt(` This terminates every workflow (${totalTargets}) and cannot be undone.`, 'destroy');
1626
1693
  if (!confirmed) {
1627
1694
  out.log('Aborted.');
1695
+ // We may have started Temporal solely to run this destroy.
1696
+ // Aborting at the confirmation prompt must not leave that
1697
+ // server orphaned — stop it before the hard exit. We own it
1698
+ // outright, so force past the cross-profile guard.
1699
+ if (startedTemporalForDestroy) {
1700
+ if (stopTemporalServer({ killSharedTemporal: true }).action === 'killed') {
1701
+ out.log(` ${out.dim('Temporal server stopped')}`);
1702
+ }
1703
+ }
1628
1704
  process.exit(0);
1629
1705
  }
1630
1706
  }
@@ -1676,7 +1752,12 @@ async function down(opts) {
1676
1752
  // skips the kill when the OPPOSITE profile is likely active;
1677
1753
  // `--kill-shared-temporal` is the explicit opt-in to override.
1678
1754
  if (temporalUp) {
1679
- const result = stopTemporalServer({ killSharedTemporal: opts.killSharedTemporal });
1755
+ // When we started Temporal ourselves just for the destroy step, always
1756
+ // stop it again — the cross-profile guard is about not killing a server
1757
+ // the *other* profile owns, but this one we own outright.
1758
+ const result = stopTemporalServer({
1759
+ killSharedTemporal: opts.killSharedTemporal || startedTemporalForDestroy,
1760
+ });
1680
1761
  switch (result.action) {
1681
1762
  case 'killed':
1682
1763
  out.success('Temporal server stopped');
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Refresh the entrypoint pointer so the global wrapper resolves to the
3
+ * currently-running binary. Called on every successful CLI boot — cheap
4
+ * (one `writeFileSync`) and idempotent.
5
+ */
6
+ export declare function refreshEntrypoint(): void;
7
+ /**
8
+ * Ensure the wrapper scripts exist. Called once per binary version (guarded
9
+ * by the bootstrap cache). Returns `true` if a PATH hint should be shown
10
+ * to the user (i.e., the bin dir is not yet on PATH).
11
+ */
12
+ export declare function provisionWrapperScripts(): {
13
+ created: boolean;
14
+ needsPathHint: boolean;
15
+ };
16
+ /**
17
+ * Returns a one-liner PATH hint appropriate for the current platform/shell.
18
+ */
19
+ export declare function getPathHint(): string;
@@ -0,0 +1,169 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.refreshEntrypoint = refreshEntrypoint;
4
+ exports.provisionWrapperScripts = provisionWrapperScripts;
5
+ exports.getPathHint = getPathHint;
6
+ /**
7
+ * Global wrapper provisioning — ensures `agent-tempo` is runnable from any
8
+ * shell without requiring `npx` or manual PATH surgery.
9
+ *
10
+ * **Strategy**: Write a thin wrapper script into `~/.agent-tempo/bin/` that
11
+ * reads an entrypoint pointer file (`.entrypoint`) to locate the real
12
+ * `dist/cli.js`. Every successful CLI boot refreshes the pointer so
13
+ * reinstalls (npm, pnpm, yarn — any package manager) auto-heal without
14
+ * user intervention.
15
+ *
16
+ * Cross-platform: emits a POSIX shell script + a `.cmd` for Windows.
17
+ *
18
+ * **Non-breaking**: If `agent-tempo` already resolves on PATH to a location
19
+ * outside `~/.agent-tempo/bin/` (e.g. npm global install), the wrapper is
20
+ * still written but the PATH hint is suppressed.
21
+ */
22
+ const fs_1 = require("fs");
23
+ const path_1 = require("path");
24
+ const os_1 = require("os");
25
+ /** Resolved CLI entrypoint — the `dist/cli.js` of the running binary. */
26
+ const THIS_CLI_JS = (0, path_1.resolve)(__dirname, '..', 'cli.js');
27
+ /**
28
+ * Wrapper bin directory. Lives inside the agent-tempo home so it follows
29
+ * the same dev-mode / home-override semantics, but we hardcode `~/.agent-tempo`
30
+ * here because the wrapper must be stable across dev/prod modes — it's a
31
+ * user-facing PATH entry that shouldn't move.
32
+ */
33
+ function getWrapperBinDir() {
34
+ return (0, path_1.join)((0, os_1.homedir)(), '.agent-tempo', 'bin');
35
+ }
36
+ const ENTRYPOINT_FILENAME = '.entrypoint';
37
+ // ─── Unix wrapper ────────────────────────────────────────────────────────
38
+ /* eslint-disable no-useless-escape */
39
+ const UNIX_WRAPPER = [
40
+ '#!/bin/sh',
41
+ '# Auto-generated by agent-tempo. Do not edit manually.',
42
+ '# This wrapper resolves the agent-tempo CLI entrypoint dynamically so',
43
+ '# reinstalls (npm/pnpm/yarn) auto-heal without re-linking.',
44
+ 'set -e',
45
+ 'SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"',
46
+ `ENTRYPOINT_FILE="\${SCRIPT_DIR}/${ENTRYPOINT_FILENAME}"`,
47
+ 'if [ ! -f "$ENTRYPOINT_FILE" ]; then',
48
+ ' echo "agent-tempo: entrypoint not configured. Run \'npx agent-tempo\' once to repair." >&2',
49
+ ' exit 1',
50
+ 'fi',
51
+ 'ENTRYPOINT="$(cat "$ENTRYPOINT_FILE")"',
52
+ 'if [ ! -f "$ENTRYPOINT" ]; then',
53
+ ' echo "agent-tempo: entrypoint stale ($ENTRYPOINT). Run \'npx agent-tempo\' once to repair." >&2',
54
+ ' exit 1',
55
+ 'fi',
56
+ 'exec node "$ENTRYPOINT" "$@"',
57
+ '',
58
+ ].join('\n');
59
+ // ─── Windows wrapper ─────────────────────────────────────────────────────
60
+ const WIN_WRAPPER = [
61
+ '@echo off',
62
+ 'rem Auto-generated by agent-tempo. Do not edit manually.',
63
+ 'setlocal enabledelayedexpansion',
64
+ 'set "SCRIPT_DIR=%~dp0"',
65
+ `set "ENTRYPOINT_FILE=%SCRIPT_DIR%${ENTRYPOINT_FILENAME}"`,
66
+ 'if not exist "%ENTRYPOINT_FILE%" (',
67
+ ' echo agent-tempo: entrypoint not configured. Run "npx agent-tempo" once to repair. >&2',
68
+ ' exit /b 1',
69
+ ')',
70
+ 'set /p ENTRYPOINT=<"%ENTRYPOINT_FILE%"',
71
+ 'if not exist "%ENTRYPOINT%" (',
72
+ ' echo agent-tempo: entrypoint stale. Run "npx agent-tempo" once to repair. >&2',
73
+ ' exit /b 1',
74
+ ')',
75
+ 'node "%ENTRYPOINT%" %*',
76
+ '',
77
+ ].join('\r\n');
78
+ // ─── Public API ──────────────────────────────────────────────────────────
79
+ /**
80
+ * Refresh the entrypoint pointer so the global wrapper resolves to the
81
+ * currently-running binary. Called on every successful CLI boot — cheap
82
+ * (one `writeFileSync`) and idempotent.
83
+ */
84
+ function refreshEntrypoint() {
85
+ try {
86
+ const binDir = getWrapperBinDir();
87
+ (0, fs_1.mkdirSync)(binDir, { recursive: true });
88
+ const pointerPath = (0, path_1.join)(binDir, ENTRYPOINT_FILENAME);
89
+ const current = safeRead(pointerPath);
90
+ // Skip write if already correct — avoids unnecessary disk churn.
91
+ if (current === THIS_CLI_JS)
92
+ return;
93
+ // Atomic write-then-rename: a torn `.entrypoint` would break the wrapper
94
+ // until the next CLI boot, so stage into a tmp file and rename into place.
95
+ // `renameSync` is atomic on POSIX and at least better-than-torn on Windows.
96
+ const tmp = pointerPath + '.tmp';
97
+ (0, fs_1.writeFileSync)(tmp, THIS_CLI_JS, 'utf8');
98
+ (0, fs_1.renameSync)(tmp, pointerPath);
99
+ }
100
+ catch {
101
+ // Best-effort — never throw from a convenience provisioning step.
102
+ }
103
+ }
104
+ /**
105
+ * Ensure the wrapper scripts exist. Called once per binary version (guarded
106
+ * by the bootstrap cache). Returns `true` if a PATH hint should be shown
107
+ * to the user (i.e., the bin dir is not yet on PATH).
108
+ */
109
+ function provisionWrapperScripts() {
110
+ try {
111
+ const binDir = getWrapperBinDir();
112
+ (0, fs_1.mkdirSync)(binDir, { recursive: true });
113
+ const unixPath = (0, path_1.join)(binDir, 'agent-tempo');
114
+ const cmdPath = (0, path_1.join)(binDir, 'agent-tempo.cmd');
115
+ let created = false;
116
+ // Write Unix wrapper if missing or outdated.
117
+ if (!(0, fs_1.existsSync)(unixPath) || safeRead(unixPath) !== UNIX_WRAPPER) {
118
+ (0, fs_1.writeFileSync)(unixPath, UNIX_WRAPPER, { mode: 0o755 });
119
+ created = true;
120
+ }
121
+ // Ensure executable even if content matches (chmod may have been lost).
122
+ try {
123
+ (0, fs_1.chmodSync)(unixPath, 0o755);
124
+ }
125
+ catch { /* Windows — no-op */ }
126
+ // Write Windows wrapper if missing or outdated.
127
+ if (!(0, fs_1.existsSync)(cmdPath) || safeRead(cmdPath) !== WIN_WRAPPER) {
128
+ (0, fs_1.writeFileSync)(cmdPath, WIN_WRAPPER);
129
+ created = true;
130
+ }
131
+ // Write the entrypoint pointer.
132
+ refreshEntrypoint();
133
+ // Determine if PATH hint is needed.
134
+ const needsPathHint = !isBinDirOnPath(binDir);
135
+ return { created, needsPathHint };
136
+ }
137
+ catch {
138
+ return { created: false, needsPathHint: false };
139
+ }
140
+ }
141
+ /**
142
+ * Returns a one-liner PATH hint appropriate for the current platform/shell.
143
+ */
144
+ function getPathHint() {
145
+ const binDir = getWrapperBinDir();
146
+ if (process.platform === 'win32') {
147
+ return `Add to PATH: setx PATH "%PATH%;${binDir}"`;
148
+ }
149
+ // Unix — suggest the export for both bash and zsh.
150
+ return `Add to PATH: export PATH="${binDir}:$PATH" (add to ~/.zshrc or ~/.bashrc)`;
151
+ }
152
+ // ─── Internals ───────────────────────────────────────────────────────────
153
+ function safeRead(filePath) {
154
+ try {
155
+ return (0, fs_1.readFileSync)(filePath, 'utf8');
156
+ }
157
+ catch {
158
+ return undefined;
159
+ }
160
+ }
161
+ function isBinDirOnPath(binDir) {
162
+ const pathEnv = process.env.PATH || process.env.Path || '';
163
+ const sep = process.platform === 'win32' ? ';' : ':';
164
+ const dirs = pathEnv.split(sep);
165
+ // Windows filesystem paths are case-insensitive; normalize before comparing
166
+ // so `C:\Users\X\...` and `c:\users\x\...` don't produce a spurious PATH hint.
167
+ const norm = (p) => process.platform === 'win32' ? (0, path_1.resolve)(p).toLowerCase() : (0, path_1.resolve)(p);
168
+ return dirs.some((d) => norm(d) === norm(binDir));
169
+ }