agent-tempo 1.7.0-beta.6 → 1.7.0-beta.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CLAUDE.md CHANGED
@@ -104,6 +104,7 @@ src/
104
104
  │ ├── hosts.ts / set-ensemble-description.ts
105
105
  │ ├── save-state.ts / fetch-state.ts / clear-state.ts
106
106
  │ ├── coat-check-put.ts / coat-check-get.ts / coat-check-list.ts / coat-check-evict.ts
107
+ │ ├── respond.ts
107
108
  │ └── descriptor.ts # Transport-neutral tool descriptor (TempoToolDescriptor) + renderToMcp; per-tool `build*Tool` factories live in each tool file (MD-B, Phase 1)
108
109
  ├── pi/ # Pi-native integration — a Pi session as a first-class player over the Temporal core
109
110
  │ ├── extension.ts # `export default function(pi)` — interactive runtime entry. Holds the MODULE-SCOPE singleton `Map<workflowId, PiPlayerRuntime>` that survives Pi's per-switch instance rebuild (rebind, not re-claim); full tool surface via renderToPi; Option-C reason-discriminated teardown
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "agent-tempo-dashboard",
3
3
  "private": true,
4
- "version": "1.7.0-beta.6",
4
+ "version": "1.7.0-beta.7",
5
5
  "type": "module",
6
6
  "description": "Web dashboard for agent-tempo. Bundled into the npm package; served by the daemon at /dashboard/*.",
7
7
  "scripts": {
@@ -72,12 +72,14 @@ async function commandCenterCommand(args) {
72
72
  process.exit(1);
73
73
  }
74
74
  // Admin (T3) token — mission-control's operator write/gate surface reads it.
75
- // Without it the board still OBSERVES (coarse SSE), but operator actions return
76
- // 401; warn rather than block so a read-only board is still useful.
75
+ // #54: a LOCAL (loopback) daemon grants full trust tokenless, so a tokenless
76
+ // board is fully functional locally; only a REMOTE / 0.0.0.0 daemon requires the
77
+ // token. Informational (not a warning, not a block) — accurate to the daemon's
78
+ // own auth posture.
77
79
  const adminToken = process.env[config_1.ENV.HTTP_ADMIN_TOKEN];
78
80
  if (!adminToken) {
79
- out.warn(`${config_1.ENV.HTTP_ADMIN_TOKEN} is not set — the board will observe read-only; operator ` +
80
- 'actions (cue/pause/restart/gate) need the admin token. Export it before launching for full control.');
81
+ out.log(out.dim(` ${config_1.ENV.HTTP_ADMIN_TOKEN} not set — fine for a local (loopback) daemon (full trust). ` +
82
+ 'Set it only if this board drives a remote / 0.0.0.0 daemon.'));
81
83
  }
82
84
  if (!process.env.ANTHROPIC_API_KEY) {
83
85
  out.warn('ANTHROPIC_API_KEY is not set — the Pi command-center will fall back to Pi\'s own auth/default model.');
package/dist/cli.js CHANGED
@@ -638,6 +638,9 @@ async function main() {
638
638
  const { installPiExtensions } = await Promise.resolve().then(() => __importStar(require('./pi/install')));
639
639
  const result = installPiExtensions({ project: args.project });
640
640
  out.success(`Pi extensions installed → ${result.settingsPath}`);
641
+ // #52 — show pruned stale/old-version entries so an upgrade is legible.
642
+ for (const p of result.removed)
643
+ out.log(` ${out.yellow('-')} ${p} ${out.dim('(removed stale/old-version entry)')}`);
641
644
  for (const p of result.added)
642
645
  out.log(` ${out.green('+')} ${p}`);
643
646
  for (const p of result.alreadyPresent)
@@ -26,9 +26,34 @@ export interface InstallPiResult {
26
26
  added: string[];
27
27
  /** Extension paths already present before this run. */
28
28
  alreadyPresent: string[];
29
+ /**
30
+ * #52 — STALE agent-tempo extension paths PRUNED by this run (old-version /
31
+ * moved-install entries that pointed at an agent-tempo extension but are no
32
+ * longer the current path). Empty on a clean re-run.
33
+ */
34
+ removed: string[];
29
35
  /** The final `extensions` array written to settings.json. */
30
36
  extensions: string[];
31
37
  }
38
+ /**
39
+ * #52 — does this settings `extensions` entry point at an agent-tempo Pi
40
+ * extension (player or command-center), of ANY version / install location?
41
+ *
42
+ * The motivating bug: a `pnpm` global install version-hashes the package dir
43
+ * (`.../.pnpm/agent-tempo@<version>_<hash>/node_modules/agent-tempo/...`), so on
44
+ * UPGRADE the recorded absolute path goes stale — and a naive add-only install
45
+ * leaves BOTH the old and new paths in `settings.json`, which makes `pi` fail on
46
+ * the now-missing stale entry. {@link installPiExtensions} prunes every match of
47
+ * this predicate (except the current paths) before re-adding, so a re-run
48
+ * REPLACES rather than duplicates.
49
+ *
50
+ * Match = an agent-tempo package marker (`/agent-tempo@…` version dir, or the
51
+ * `/node_modules/agent-tempo/` package dir) AND an agent-tempo extension suffix
52
+ * (`dist/pi/extension.js` or `dist/pi/mission-control/extension.js`). Both halves
53
+ * are required so a user's own unrelated extension is never pruned. Separators
54
+ * are normalised so the predicate holds on Windows paths too.
55
+ */
56
+ export declare function isAgentTempoExtensionPath(p: string): boolean;
32
57
  /** Resolve the Pi settings.json path for the chosen scope. */
33
58
  export declare function piSettingsPath(opts?: InstallPiOptions): string;
34
59
  /**
@@ -37,6 +62,13 @@ export declare function piSettingsPath(opts?: InstallPiOptions): string;
37
62
  * write when nothing changed). Never copies any extension file — install by
38
63
  * reference only (see file header).
39
64
  *
65
+ * #52 — REPLACE, don't accumulate: before adding the current paths, PRUNE any
66
+ * STALE agent-tempo extension entries ({@link isAgentTempoExtensionPath}, minus
67
+ * the current paths). On a `pnpm` upgrade the package dir is version-hashed, so
68
+ * the recorded absolute path changes — without pruning, `settings.json` would
69
+ * list both the old (now-missing) and new paths and `pi` would fail on the stale
70
+ * one. A user's own unrelated extensions and other settings keys are preserved.
71
+ *
40
72
  * Tolerates a missing / empty / corrupt settings file: a missing file is
41
73
  * created; an unparseable one is replaced with a fresh object carrying just the
42
74
  * extensions (we can only safely merge a valid object). Other recognised keys in
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.piExtensionPaths = piExtensionPaths;
4
+ exports.isAgentTempoExtensionPath = isAgentTempoExtensionPath;
4
5
  exports.piSettingsPath = piSettingsPath;
5
6
  exports.installPiExtensions = installPiExtensions;
6
7
  /**
@@ -45,6 +46,30 @@ function piExtensionPaths() {
45
46
  missionControl: (0, path_1.resolve)(__dirname, 'mission-control', 'extension.js'),
46
47
  };
47
48
  }
49
+ /**
50
+ * #52 — does this settings `extensions` entry point at an agent-tempo Pi
51
+ * extension (player or command-center), of ANY version / install location?
52
+ *
53
+ * The motivating bug: a `pnpm` global install version-hashes the package dir
54
+ * (`.../.pnpm/agent-tempo@<version>_<hash>/node_modules/agent-tempo/...`), so on
55
+ * UPGRADE the recorded absolute path goes stale — and a naive add-only install
56
+ * leaves BOTH the old and new paths in `settings.json`, which makes `pi` fail on
57
+ * the now-missing stale entry. {@link installPiExtensions} prunes every match of
58
+ * this predicate (except the current paths) before re-adding, so a re-run
59
+ * REPLACES rather than duplicates.
60
+ *
61
+ * Match = an agent-tempo package marker (`/agent-tempo@…` version dir, or the
62
+ * `/node_modules/agent-tempo/` package dir) AND an agent-tempo extension suffix
63
+ * (`dist/pi/extension.js` or `dist/pi/mission-control/extension.js`). Both halves
64
+ * are required so a user's own unrelated extension is never pruned. Separators
65
+ * are normalised so the predicate holds on Windows paths too.
66
+ */
67
+ function isAgentTempoExtensionPath(p) {
68
+ const n = p.replace(/\\/g, '/');
69
+ const fromAgentTempo = n.includes('/agent-tempo@') || n.includes('/node_modules/agent-tempo/');
70
+ const isExtensionEntry = n.endsWith('/dist/pi/extension.js') || n.endsWith('/dist/pi/mission-control/extension.js');
71
+ return fromAgentTempo && isExtensionEntry;
72
+ }
48
73
  /** Resolve the Pi settings.json path for the chosen scope. */
49
74
  function piSettingsPath(opts = {}) {
50
75
  if (opts.project)
@@ -57,6 +82,13 @@ function piSettingsPath(opts = {}) {
57
82
  * write when nothing changed). Never copies any extension file — install by
58
83
  * reference only (see file header).
59
84
  *
85
+ * #52 — REPLACE, don't accumulate: before adding the current paths, PRUNE any
86
+ * STALE agent-tempo extension entries ({@link isAgentTempoExtensionPath}, minus
87
+ * the current paths). On a `pnpm` upgrade the package dir is version-hashed, so
88
+ * the recorded absolute path changes — without pruning, `settings.json` would
89
+ * list both the old (now-missing) and new paths and `pi` would fail on the stale
90
+ * one. A user's own unrelated extensions and other settings keys are preserved.
91
+ *
60
92
  * Tolerates a missing / empty / corrupt settings file: a missing file is
61
93
  * created; an unparseable one is replaced with a fresh object carrying just the
62
94
  * extensions (we can only safely merge a valid object). Other recognised keys in
@@ -84,9 +116,15 @@ function installPiExtensions(opts = {}) {
84
116
  const current = Array.isArray(settings.extensions)
85
117
  ? settings.extensions.filter((x) => typeof x === 'string')
86
118
  : [];
119
+ // #52 — prune STALE agent-tempo extension entries (an agent-tempo extension
120
+ // path that is NOT one of the current `want` paths — e.g. an old version-hashed
121
+ // pnpm dir). The current paths and all non-agent-tempo entries keep their
122
+ // original positions.
123
+ const removed = current.filter((p) => !want.includes(p) && isAgentTempoExtensionPath(p));
124
+ const removedSet = new Set(removed);
87
125
  const added = [];
88
126
  const alreadyPresent = [];
89
- const merged = [...current];
127
+ const merged = current.filter((p) => !removedSet.has(p));
90
128
  for (const p of want) {
91
129
  if (merged.includes(p)) {
92
130
  alreadyPresent.push(p);
@@ -97,11 +135,11 @@ function installPiExtensions(opts = {}) {
97
135
  }
98
136
  }
99
137
  settings.extensions = merged;
100
- // Idempotent: only write when something actually changed (or the file is
101
- // absent and must be created). A clean repeat run touches nothing.
102
- if (added.length > 0 || !fileExists) {
138
+ // Idempotent: only write when something actually changed (added, pruned, or the
139
+ // file is absent and must be created). A clean repeat run touches nothing.
140
+ if (added.length > 0 || removed.length > 0 || !fileExists) {
103
141
  (0, fs_1.mkdirSync)((0, path_1.dirname)(settingsPath), { recursive: true });
104
142
  (0, fs_1.writeFileSync)(settingsPath, `${JSON.stringify(settings, null, 2)}\n`, 'utf8');
105
143
  }
106
- return { settingsPath, added, alreadyPresent, extensions: merged };
144
+ return { settingsPath, added, alreadyPresent, removed, extensions: merged };
107
145
  }
@@ -32,14 +32,35 @@ export declare class MissionControlActions {
32
32
  private readonly baseUrlOverride;
33
33
  private readonly fetchFn;
34
34
  constructor(opts: MissionControlActionsOptions);
35
- /** Whether the client is usable (token + transport present). */
35
+ /**
36
+ * Whether the client has a usable transport. (#54) NO LONGER gates on the admin
37
+ * token: a loopback daemon grants full trust tokenless, so token presence does
38
+ * NOT determine usability — the daemon decides per request. Token-required is
39
+ * enforced by the daemon (it 401s a remote/0.0.0.0 caller), not pre-empted here.
40
+ */
36
41
  get ready(): boolean;
37
42
  private baseUrl;
43
+ /**
44
+ * Request headers — include the admin bearer ONLY when a token is set (#54). A
45
+ * loopback daemon grants full trust tokenless (it short-circuits all tiers), so
46
+ * we attempt tokenless and never send a literal "Bearer undefined". Mirrors how
47
+ * `createSubscribe` already spreads its token only when present.
48
+ */
49
+ private authHeaders;
50
+ /**
51
+ * Map a non-2xx daemon response to an error string (#54). When NO token was sent
52
+ * and the daemon rejected on auth (401/403) or admin-unset (503), the cause is a
53
+ * remote / `0.0.0.0` daemon that requires the admin token — surface that
54
+ * actionably (a local loopback daemon needs none). Token-present failures keep
55
+ * the daemon's own body detail (it already returns good 403/503 hints).
56
+ */
57
+ private httpError;
38
58
  private post;
39
- /** POST and parse a JSON response body (bearer-authed). Used when the caller
40
- * needs the response payload, not just success — e.g. the coat-check ticket. */
59
+ /** POST and parse a JSON response body. Used when the caller needs the response
60
+ * payload, not just success — e.g. the coat-check ticket. Bearer iff token set (#54). */
41
61
  private postJson;
42
- /** GET a JSON body from the daemon (bearer-authed). Used by the read surface (#700 readAnswer). */
62
+ /** GET a JSON body from the daemon. Used by the read surface (#700 readAnswer).
63
+ * Bearer iff token set (#54). */
43
64
  private getJson;
44
65
  private ens;
45
66
  private player;
@@ -29,9 +29,14 @@ class MissionControlActions {
29
29
  this.baseUrlOverride = opts.baseUrl;
30
30
  this.fetchFn = opts.fetchFn ?? resolveFetch();
31
31
  }
32
- /** Whether the client is usable (token + transport present). */
32
+ /**
33
+ * Whether the client has a usable transport. (#54) NO LONGER gates on the admin
34
+ * token: a loopback daemon grants full trust tokenless, so token presence does
35
+ * NOT determine usability — the daemon decides per request. Token-required is
36
+ * enforced by the daemon (it 401s a remote/0.0.0.0 caller), not pre-empted here.
37
+ */
33
38
  get ready() {
34
- return Boolean(this.adminToken) && this.fetchFn !== null;
39
+ return this.fetchFn !== null;
35
40
  }
36
41
  baseUrl() {
37
42
  if (this.baseUrlOverride)
@@ -39,9 +44,32 @@ class MissionControlActions {
39
44
  const port = (0, port_file_1.readPortFile)() ?? DEFAULT_PORT;
40
45
  return `http://127.0.0.1:${port}`;
41
46
  }
47
+ /**
48
+ * Request headers — include the admin bearer ONLY when a token is set (#54). A
49
+ * loopback daemon grants full trust tokenless (it short-circuits all tiers), so
50
+ * we attempt tokenless and never send a literal "Bearer undefined". Mirrors how
51
+ * `createSubscribe` already spreads its token only when present.
52
+ */
53
+ authHeaders(extra = {}) {
54
+ return { ...extra, ...(this.adminToken ? { Authorization: `Bearer ${this.adminToken}` } : {}) };
55
+ }
56
+ /**
57
+ * Map a non-2xx daemon response to an error string (#54). When NO token was sent
58
+ * and the daemon rejected on auth (401/403) or admin-unset (503), the cause is a
59
+ * remote / `0.0.0.0` daemon that requires the admin token — surface that
60
+ * actionably (a local loopback daemon needs none). Token-present failures keep
61
+ * the daemon's own body detail (it already returns good 403/503 hints).
62
+ */
63
+ httpError(status, detail) {
64
+ if (!this.adminToken && (status === 401 || status === 403 || status === 503)) {
65
+ return (`HTTP ${status}: operator actions need ${exports.ADMIN_TOKEN_ENV} for a remote / 0.0.0.0 daemon ` +
66
+ `(a local loopback daemon needs none)${detail ? ` — ${detail}` : ''}`);
67
+ }
68
+ return `HTTP ${status}${detail ? `: ${detail}` : ''}`;
69
+ }
42
70
  async post(pathSuffix, body) {
43
- if (!this.adminToken)
44
- return { ok: false, error: `no admin token (set ${exports.ADMIN_TOKEN_ENV})` };
71
+ // #54 — do NOT pre-block on a missing token: attempt the request and let the
72
+ // daemon decide (loopback grants full trust tokenless; remote/0.0.0.0 401s).
45
73
  if (!this.fetchFn)
46
74
  return { ok: false, error: 'no fetch transport available' };
47
75
  const base = this.baseUrl();
@@ -50,23 +78,21 @@ class MissionControlActions {
50
78
  try {
51
79
  const res = await this.fetchFn(`${base}${pathSuffix}`, {
52
80
  method: 'POST',
53
- headers: { Authorization: `Bearer ${this.adminToken}`, 'Content-Type': 'application/json' },
81
+ headers: this.authHeaders({ 'Content-Type': 'application/json' }),
54
82
  body: JSON.stringify(body ?? {}),
55
83
  });
56
84
  if (res.status >= 200 && res.status < 300)
57
85
  return { ok: true, status: res.status };
58
86
  const detail = (await res.text().catch(() => '')).slice(0, 200);
59
- return { ok: false, error: `HTTP ${res.status}${detail ? `: ${detail}` : ''}` };
87
+ return { ok: false, error: this.httpError(res.status, detail) };
60
88
  }
61
89
  catch (err) {
62
90
  return { ok: false, error: err instanceof Error ? err.message : String(err) };
63
91
  }
64
92
  }
65
- /** POST and parse a JSON response body (bearer-authed). Used when the caller
66
- * needs the response payload, not just success — e.g. the coat-check ticket. */
93
+ /** POST and parse a JSON response body. Used when the caller needs the response
94
+ * payload, not just success — e.g. the coat-check ticket. Bearer iff token set (#54). */
67
95
  async postJson(pathSuffix, body) {
68
- if (!this.adminToken)
69
- return { ok: false, error: `no admin token (set ${exports.ADMIN_TOKEN_ENV})` };
70
96
  if (!this.fetchFn)
71
97
  return { ok: false, error: 'no fetch transport available' };
72
98
  const base = this.baseUrl();
@@ -75,22 +101,21 @@ class MissionControlActions {
75
101
  try {
76
102
  const res = await this.fetchFn(`${base}${pathSuffix}`, {
77
103
  method: 'POST',
78
- headers: { Authorization: `Bearer ${this.adminToken}`, 'Content-Type': 'application/json' },
104
+ headers: this.authHeaders({ 'Content-Type': 'application/json' }),
79
105
  body: JSON.stringify(body ?? {}),
80
106
  });
81
107
  const text = await res.text().catch(() => '');
82
108
  if (res.status < 200 || res.status >= 300)
83
- return { ok: false, error: `HTTP ${res.status}${text ? `: ${text.slice(0, 200)}` : ''}` };
109
+ return { ok: false, error: this.httpError(res.status, text.slice(0, 200)) };
84
110
  return { ok: true, data: JSON.parse(text) };
85
111
  }
86
112
  catch (err) {
87
113
  return { ok: false, error: err instanceof Error ? err.message : String(err) };
88
114
  }
89
115
  }
90
- /** GET a JSON body from the daemon (bearer-authed). Used by the read surface (#700 readAnswer). */
116
+ /** GET a JSON body from the daemon. Used by the read surface (#700 readAnswer).
117
+ * Bearer iff token set (#54). */
91
118
  async getJson(pathSuffix) {
92
- if (!this.adminToken)
93
- return { ok: false, error: `no admin token (set ${exports.ADMIN_TOKEN_ENV})` };
94
119
  if (!this.fetchFn)
95
120
  return { ok: false, error: 'no fetch transport available' };
96
121
  const base = this.baseUrl();
@@ -99,11 +124,11 @@ class MissionControlActions {
99
124
  try {
100
125
  const res = await this.fetchFn(`${base}${pathSuffix}`, {
101
126
  method: 'GET',
102
- headers: { Authorization: `Bearer ${this.adminToken}` },
127
+ headers: this.authHeaders(),
103
128
  });
104
129
  const text = await res.text().catch(() => '');
105
130
  if (res.status < 200 || res.status >= 300)
106
- return { ok: false, error: `HTTP ${res.status}${text ? `: ${text.slice(0, 200)}` : ''}` };
131
+ return { ok: false, error: this.httpError(res.status, text.slice(0, 200)) };
107
132
  return { ok: true, data: JSON.parse(text) };
108
133
  }
109
134
  catch (err) {
@@ -564,8 +564,11 @@ function createMissionControlExtension(deps = {}) {
564
564
  activeCtx.ui.setWidget(WIDGET_KEY, (0, render_1.renderBoard)(ctrl.model, ctrl.localHost), { placement: 'aboveEditor' });
565
565
  };
566
566
  const startCoarse = () => {
567
+ // #54 — accurate posture: a tokenless board is FULLY functional against a
568
+ // local (loopback) daemon, which grants full trust. Only a REMOTE / 0.0.0.0
569
+ // daemon requires the admin token (it 401s tokenless reads + actions).
567
570
  if (!adminToken) {
568
- log(`no admin token (${actions_1.ADMIN_TOKEN_ENV})board limited / disabled`);
571
+ log(`no ${actions_1.ADMIN_TOKEN_ENV} — OK for a local loopback daemon (full trust); a remote / 0.0.0.0 daemon will require it`);
569
572
  }
570
573
  // H5: capture the controller locally. teardown nulls the outer `coarseAbort`,
571
574
  // so the catch must check THIS signal — checking the nulled outer ref made an
@@ -604,7 +607,9 @@ function createMissionControlExtension(deps = {}) {
604
607
  const openTail = (playerId) => {
605
608
  tailAbort?.abort();
606
609
  tailAbort = null;
607
- if (playerId === null || !adminToken)
610
+ // #54 do NOT gate on the token: the loopback daemon serves /inner tokenless.
611
+ // openInnerTail sends the bearer iff present; a remote/0.0.0.0 daemon 401s → onError.
612
+ if (playerId === null)
608
613
  return;
609
614
  tailAbort = new AbortController();
610
615
  // H5: resolve the daemon base URL HERE (per /tail) so a port change is
@@ -32,7 +32,13 @@ export type TailFetch = (url: string, init: {
32
32
  }>;
33
33
  export interface OpenInnerTailOptions {
34
34
  baseUrl: string;
35
- adminToken: string;
35
+ /**
36
+ * Admin (T3) token. OPTIONAL (#54): a loopback daemon serves the `/inner`
37
+ * egress tokenless (full-trust short-circuit). When absent, no `Authorization`
38
+ * header is sent and the daemon decides — a remote / `0.0.0.0` daemon 401s
39
+ * (surfaced via `onError`).
40
+ */
41
+ adminToken?: string;
36
42
  ensemble: string;
37
43
  playerId: string;
38
44
  onFrame: (frame: InnerFrame) => void;
@@ -44,7 +44,11 @@ async function openInnerTail(opts) {
44
44
  try {
45
45
  res = await opts.fetchFn(url, {
46
46
  method: 'GET',
47
- headers: { Authorization: `Bearer ${opts.adminToken}`, Accept: 'text/event-stream' },
47
+ headers: {
48
+ Accept: 'text/event-stream',
49
+ // #54 — bearer ONLY when a token is set; loopback serves tokenless.
50
+ ...(opts.adminToken ? { Authorization: `Bearer ${opts.adminToken}` } : {}),
51
+ },
48
52
  signal: opts.signal,
49
53
  });
50
54
  }
@@ -54,7 +58,11 @@ async function openInnerTail(opts) {
54
58
  return;
55
59
  }
56
60
  if (res.status !== 200 || !res.body) {
57
- opts.onError?.(`inner tail HTTP ${res.status}`);
61
+ // #54 — a tokenless 401/403 means a remote / 0.0.0.0 daemon that needs the token.
62
+ const hint = !opts.adminToken && (res.status === 401 || res.status === 403)
63
+ ? ' (set AGENT_TEMPO_HTTP_ADMIN_TOKEN for a remote/0.0.0.0 daemon; loopback needs none)'
64
+ : '';
65
+ opts.onError?.(`inner tail HTTP ${res.status}${hint}`);
58
66
  return;
59
67
  }
60
68
  const decoder = new TextDecoder();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-tempo",
3
- "version": "1.7.0-beta.6",
3
+ "version": "1.7.0-beta.7",
4
4
  "description": "Many agents, one tempo. Durable coordination for multi-agent work via Temporal.",
5
5
  "keywords": [
6
6
  "mcp",