@todoforai/cli 0.1.54 → 0.1.55

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/README.md CHANGED
@@ -70,6 +70,17 @@ todoforai-cli -c # continue most recent todo
70
70
  todoforai-cli --resume <todo-id> # resume specific todo
71
71
  ```
72
72
 
73
+ ### Notifications
74
+
75
+ ```bash
76
+ tfa-cli inbox # the bell: newest across all tabs
77
+ tfa-cli inbox messages --unread # one filter: needs-you | messages | activity
78
+ tfa-cli inbox seen # mark everything read
79
+ tfa-cli notify "Deploy done" "v2 is live" --href /t/<todo-id> # note to YOUR user (Messages tab + phone push)
80
+ ```
81
+
82
+ `notify` has no recipient option: it always goes to the user the key belongs to (never a teammate or other account), is shown as from the calling agent, delivers the same text once (`--subject` to control the dedupe key) and is capped at 10 new notes per day.
83
+
73
84
  ## IDE integration (ACP)
74
85
 
75
86
  `todoforai-cli acp` speaks the [Agent Client Protocol](https://agentclientprotocol.com) over stdio, so any ACP-capable editor can drive your agent: prompts from the editor chat, file edits as native diffs, permission prompts inline, and shell commands running on your machine through the bridge. One ACP thread = one todo. Requires `todoforai-cli login` first; `--isolated` / `--user-id` are not supported in this mode.
@@ -42745,7 +42745,7 @@ function randomTip() {
42745
42745
  return TIPS[Math.floor(Math.random() * TIPS.length)];
42746
42746
  }
42747
42747
 
42748
- // ../packages/shared-api/src/api.ts
42748
+ // ../../../home/hm/repo/todoforai/packages/shared-api/src/api.ts
42749
42749
  function restBasePath(apiKey) {
42750
42750
  return apiKey.startsWith("dst_") ? "/dst/v1" : "/api/v1";
42751
42751
  }
@@ -42926,6 +42926,19 @@ class ApiClient {
42926
42926
  upsertProjectGroup(projectId, slug, fields) {
42927
42927
  return this.request("PUT", `/api/v1/projects/${projectId}/groups/${encodeURIComponent(slug)}`, { projectId, slug, ...fields });
42928
42928
  }
42929
+ listFeed(opts = {}) {
42930
+ const qs = new URLSearchParams({ all: "true" });
42931
+ for (const [k, v] of Object.entries(opts))
42932
+ if (v !== undefined)
42933
+ qs.set(k, String(v));
42934
+ return this.request("GET", `/api/v1/feed?${qs}`);
42935
+ }
42936
+ markFeedSeen(opts = {}) {
42937
+ return this.request("POST", "/api/v1/feed/seen", { all: true, ...opts });
42938
+ }
42939
+ sendFeedMessage(msg) {
42940
+ return this.request("POST", "/api/v1/feed/message", msg);
42941
+ }
42929
42942
  listProjectSurfaces(projectId) {
42930
42943
  return this.request("GET", `/api/v1/projects/${projectId}/surfaces`);
42931
42944
  }
@@ -43156,11 +43169,11 @@ class ApiClient {
43156
43169
  return res.json();
43157
43170
  }
43158
43171
  }
43159
- // ../packages/shared-api/src/frontend-ws.ts
43172
+ // ../../../home/hm/repo/todoforai/packages/shared-api/src/frontend-ws.ts
43160
43173
  import { randomUUID } from "crypto";
43161
43174
  import WebSocket from "ws";
43162
43175
 
43163
- // ../packages/shared-credentials/src/index.ts
43176
+ // ../../../home/hm/repo/todoforai/packages/shared-credentials/src/index.ts
43164
43177
  import { readFileSync, writeFileSync, mkdirSync, chmodSync } from "node:fs";
43165
43178
  import { join, dirname } from "node:path";
43166
43179
  import { homedir, platform } from "node:os";
@@ -43200,13 +43213,14 @@ function writeCredential(apiUrl, apiKey) {
43200
43213
  writeSecure(c);
43201
43214
  }
43202
43215
 
43203
- // ../packages/shared-api/src/frontend-ws.ts
43216
+ // ../../../home/hm/repo/todoforai/packages/shared-api/src/frontend-ws.ts
43204
43217
  var log = (level, ...args) => console.log(`[frontend-ws:${level}]`, ...args);
43205
43218
 
43206
43219
  class FrontendWebSocket {
43207
43220
  apiUrl;
43208
43221
  apiKey;
43209
43222
  ws = null;
43223
+ connecting = null;
43210
43224
  tabId = randomUUID();
43211
43225
  connected = false;
43212
43226
  callbacks = new Map;
@@ -43230,28 +43244,61 @@ class FrontendWebSocket {
43230
43244
  wsUrl = `wss://${url}`;
43231
43245
  return `${wsUrl}/ws/v1/frontend?tabId=${this.tabId}`;
43232
43246
  }
43233
- async connect() {
43247
+ connect() {
43234
43248
  if (this.connected && this.ws)
43235
- return true;
43249
+ return Promise.resolve(true);
43250
+ this.connecting ??= this.open().finally(() => {
43251
+ this.connecting = null;
43252
+ });
43253
+ return this.connecting;
43254
+ }
43255
+ open() {
43236
43256
  return new Promise((resolve) => {
43237
43257
  const wsUrl = this.getWsUrl();
43238
- this.ws = new WebSocket(wsUrl, [this.apiKey], { maxPayload: 5 * 1024 * 1024 });
43239
- this.ws.on("open", () => {
43258
+ const sock = new WebSocket(wsUrl, [this.apiKey], { maxPayload: 5 * 1024 * 1024, handshakeTimeout: 1e4 });
43259
+ this.ws = sock;
43260
+ const current = () => this.ws === sock;
43261
+ let alive = true;
43262
+ let heartbeat;
43263
+ sock.on("pong", () => {
43264
+ alive = true;
43265
+ });
43266
+ sock.on("open", () => {
43267
+ if (!current())
43268
+ return;
43240
43269
  this.connected = true;
43270
+ heartbeat = setInterval(() => {
43271
+ if (!alive) {
43272
+ sock.terminate();
43273
+ return;
43274
+ }
43275
+ alive = false;
43276
+ try {
43277
+ sock.ping();
43278
+ } catch {}
43279
+ }, 15000);
43280
+ heartbeat.unref?.();
43241
43281
  resolve(true);
43242
43282
  });
43243
- this.ws.on("message", (data) => {
43283
+ sock.on("message", (data) => {
43284
+ if (!current())
43285
+ return;
43244
43286
  try {
43245
43287
  this.handleMessage(JSON.parse(data.toString()));
43246
43288
  } catch {}
43247
43289
  });
43248
- this.ws.on("close", () => {
43290
+ sock.on("close", () => {
43291
+ clearInterval(heartbeat);
43292
+ if (!current())
43293
+ return;
43249
43294
  this.connected = false;
43250
43295
  this.settleAll();
43296
+ resolve(false);
43251
43297
  });
43252
- this.ws.on("error", (err) => {
43298
+ sock.on("error", (err) => {
43253
43299
  log("error", "WS error:", err.message);
43254
- this.connected = false;
43300
+ if (current())
43301
+ this.connected = false;
43255
43302
  resolve(false);
43256
43303
  });
43257
43304
  });
@@ -43320,7 +43367,7 @@ class FrontendWebSocket {
43320
43367
  }
43321
43368
  }
43322
43369
  }
43323
- async subscribe(todoId, callback) {
43370
+ async subscribe(todoId, callback, opts = {}) {
43324
43371
  if (!this.connected && !await this.connect())
43325
43372
  return false;
43326
43373
  if (callback)
@@ -43328,8 +43375,16 @@ class FrontendWebSocket {
43328
43375
  const res = await fetch(`${this.apiUrl}/api/v1/todos/${todoId}/subscribe`, {
43329
43376
  method: "POST",
43330
43377
  headers: { "x-api-key": this.apiKey, "x-tab-id": this.tabId, "Content-Type": "application/json" },
43331
- body: JSON.stringify({ todoId })
43378
+ body: JSON.stringify({ todoId }),
43379
+ signal: AbortSignal.timeout(15000)
43332
43380
  });
43381
+ if (res.ok && opts.replayStatus) {
43382
+ this.pendingBlocks.delete(todoId);
43383
+ this.lastReadyPayload.delete(todoId);
43384
+ const status = (await res.json().catch(() => null))?.status;
43385
+ if (status)
43386
+ this.handleMessage({ type: "todo:status", payload: { todoId, status } });
43387
+ }
43333
43388
  return res.ok;
43334
43389
  }
43335
43390
  setCallback(todoId, callback) {
@@ -43339,17 +43394,16 @@ class FrontendWebSocket {
43339
43394
  this.callbacks.delete(todoId);
43340
43395
  }
43341
43396
  async sendInterrupt(projectId, todoId) {
43342
- if (!this.connected || !this.ws)
43343
- return false;
43344
- try {
43345
- this.ws.send(JSON.stringify({
43346
- type: "todo:interrupt_signal",
43347
- payload: { projectId, todoId }
43348
- }));
43349
- return true;
43350
- } catch {
43397
+ const ws = this.ws;
43398
+ if (!this.connected || !ws)
43351
43399
  return false;
43352
- }
43400
+ return new Promise((resolve) => {
43401
+ try {
43402
+ ws.send(JSON.stringify({ type: "todo:interrupt_signal", payload: { projectId, todoId } }), (err) => resolve(!err));
43403
+ } catch {
43404
+ resolve(false);
43405
+ }
43406
+ });
43353
43407
  }
43354
43408
  async sendQueueCancel(todoId, messageId) {
43355
43409
  if (!this.connected || !this.ws)
@@ -43407,6 +43461,10 @@ class FrontendWebSocket {
43407
43461
  return this.completion(todoId);
43408
43462
  }
43409
43463
  completion(todoId) {
43464
+ if (!this.connected && !this.completionResults.has(todoId)) {
43465
+ this.callbacks.delete(todoId);
43466
+ return Promise.resolve({ type: "socket:closed", payload: { todoId, status: "DISCONNECTED" }, success: false });
43467
+ }
43410
43468
  const early = this.completionResults.get(todoId);
43411
43469
  if (early) {
43412
43470
  this.completionResults.delete(todoId);
@@ -43425,7 +43483,7 @@ class FrontendWebSocket {
43425
43483
  });
43426
43484
  }
43427
43485
  }
43428
- // ../packages/shared-fbe/src/enums.ts
43486
+ // ../../../home/hm/repo/todoforai/packages/shared-fbe/src/enums.ts
43429
43487
  var TodoStatus;
43430
43488
  ((TodoStatus2) => {
43431
43489
  TodoStatus2["PROPOSED"] = "PROPOSED";
@@ -43449,7 +43507,7 @@ var TodoStatus;
43449
43507
  TodoStatus2["ARCHIVED"] = "ARCHIVED";
43450
43508
  TodoStatus2["DELETED"] = "DELETED";
43451
43509
  })(TodoStatus ||= {});
43452
- // ../packages/shared-fbe/src/models.ts
43510
+ // ../../../home/hm/repo/todoforai/packages/shared-fbe/src/models.ts
43453
43511
  var DEFAULT_MODEL = "anthropic:anthropic/claude-opus-5.5";
43454
43512
  var MODEL_ALIASES = {
43455
43513
  claude: DEFAULT_MODEL
@@ -43520,7 +43578,7 @@ function qualifiedModelIds(listing, filter) {
43520
43578
  }
43521
43579
  return q ? ids.filter((id) => id.toLowerCase().includes(q)) : ids;
43522
43580
  }
43523
- // ../packages/shared-fbe/src/billing.ts
43581
+ // ../../../home/hm/repo/todoforai/packages/shared-fbe/src/billing.ts
43524
43582
  var USAGE_WINDOWS = {
43525
43583
  SESSION_MS: 5 * 60 * 60 * 1000,
43526
43584
  WEEK_MS: 7 * 24 * 60 * 60 * 1000,
@@ -43528,7 +43586,7 @@ var USAGE_WINDOWS = {
43528
43586
  SESSION_FACTOR: 1 / 5
43529
43587
  };
43530
43588
  var NEVER_SCHEDULED_TIMESTAMP = 8640000000000000;
43531
- // ../packages/shared-fbe/src/mimetypes.json
43589
+ // ../../../home/hm/repo/todoforai/packages/shared-fbe/src/mimetypes.json
43532
43590
  var mimetypes_default = {
43533
43591
  conversionProfiles: {
43534
43592
  to_jpeg_90: { toMime: "image/jpeg", toExt: "jpg", quality: 0.9 }
@@ -43621,7 +43679,7 @@ var mimetypes_default = {
43621
43679
  }
43622
43680
  };
43623
43681
 
43624
- // ../packages/shared-fbe/src/attachmentUtils.ts
43682
+ // ../../../home/hm/repo/todoforai/packages/shared-fbe/src/attachmentUtils.ts
43625
43683
  var MCP_TEXT_MIME = Object.entries(mimetypes_default.aliases).find(([, type]) => type === "mcp_text")[0];
43626
43684
  var MCP_AUDIO_MIME = Object.entries(mimetypes_default.aliases).find(([, type]) => type === "mcp_audio")[0];
43627
43685
  var AttachmentType = Object.fromEntries(Object.keys(mimetypes_default.types).map((k) => [k.toUpperCase(), k]));
@@ -43661,7 +43719,7 @@ for (const [ext, entry] of Object.entries(mimetypes_default.extensions)) {
43661
43719
  if (!MIME_TO_EXT[mime])
43662
43720
  MIME_TO_EXT[mime] = ext;
43663
43721
  }
43664
- // ../packages/shared-fbe/src/edgeTypes.ts
43722
+ // ../../../home/hm/repo/todoforai/packages/shared-fbe/src/edgeTypes.ts
43665
43723
  var NON_SHELL_TYPES = new Set([
43666
43724
  "WEB_EXTENSION" /* WEB_EXTENSION */,
43667
43725
  "EXTENSION" /* EXTENSION */,
@@ -43669,7 +43727,7 @@ var NON_SHELL_TYPES = new Set([
43669
43727
  "ANDROID" /* ANDROID */,
43670
43728
  "IOS" /* IOS */
43671
43729
  ]);
43672
- // ../packages/shared-fbe/src/blocks/index.ts
43730
+ // ../../../home/hm/repo/todoforai/packages/shared-fbe/src/blocks/index.ts
43673
43731
  var USER_INPUT_BLOCK_TYPES = new Set([
43674
43732
  "question" /* Question */,
43675
43733
  "login_prompt" /* Login */,
@@ -43723,7 +43781,7 @@ var FILE_TOOL_TYPES = new Set([
43723
43781
  "edit" /* Edit */,
43724
43782
  "modify_file" /* ModifyFile */
43725
43783
  ]);
43726
- // ../packages/shared-fbe/src/channels.ts
43784
+ // ../../../home/hm/repo/todoforai/packages/shared-fbe/src/channels.ts
43727
43785
  var privateSuffix = (privateOwnerId) => privateOwnerId ? `:private:${privateOwnerId}` : "";
43728
43786
  var SERVER_TO_FRONTENDS = {
43729
43787
  task: {
@@ -43782,6 +43840,7 @@ var SERVER_TO_FRONTENDS = {
43782
43840
  msgMetaAi: (todoId) => `${"todo:msg_meta_ai" /* TODO_MSG_META_AI */}:${todoId}`,
43783
43841
  msgQueueUpdate: (todoId) => `${"todo:msg_queue_update" /* TODO_MSG_QUEUE_UPDATE */}:${todoId}`,
43784
43842
  message_removed: (todoId) => `${"todo:message_removed" /* TODO_MESSAGE_REMOVED */}:${todoId}`,
43843
+ commentUpdate: (todoId) => `${"todo:comment_update" /* TODO_COMMENT_UPDATE */}:${todoId}`,
43785
43844
  start_universal: (todoId) => `${"block:start_universal" /* BLOCK_START_UNIVERSAL */}:${todoId}`,
43786
43845
  message: (todoId) => `${"block:message" /* BLOCK_MESSAGE */}:${todoId}`,
43787
43846
  end: (todoId) => `${"block:end" /* BLOCK_END */}:${todoId}`,
@@ -43812,7 +43871,7 @@ var SERVER_TO_FRONTENDS = {
43812
43871
  function_call_request: (userId) => `${"FUNCTION_CALL_REQUEST_AGENT" /* FUNCTION_CALL_REQUEST_AGENT */}:frontend:${userId}`
43813
43872
  }
43814
43873
  };
43815
- // ../packages/shared-fbe/src/REST_types.ts
43874
+ // ../../../home/hm/repo/todoforai/packages/shared-fbe/src/REST_types.ts
43816
43875
  var MANUAL_CHANNEL = "Manual";
43817
43876
  function manualAnswersOf(sources) {
43818
43877
  const manual = sources?.find((s) => s.channel === MANUAL_CHANNEL);
@@ -43823,7 +43882,8 @@ function manualAnswersOf(sources) {
43823
43882
  return out;
43824
43883
  }
43825
43884
  var manualSamples = (answers) => Object.entries(answers).filter(([, a]) => a.trim()).map(([prompt, text]) => ({ prompt, text: text.trim() }));
43826
- // ../packages/shared-fbe/src/templateBody.ts
43885
+ var senderLabel = (s) => s.kind === "team" ? `${s.name} · TODO for AI team` : `${s.name} · your agent`;
43886
+ // ../../../home/hm/repo/todoforai/packages/shared-fbe/src/templateBody.ts
43827
43887
  var USER_HEADINGS = new Set([
43828
43888
  "your task",
43829
43889
  "your tasks",
@@ -43832,7 +43892,7 @@ var USER_HEADINGS = new Set([
43832
43892
  "steps",
43833
43893
  "data needed"
43834
43894
  ]);
43835
- // ../packages/shared-fbe/src/bashPatterns.ts
43895
+ // ../../../home/hm/repo/todoforai/packages/shared-fbe/src/bashPatterns.ts
43836
43896
  function splitShellCommands(input) {
43837
43897
  const parts = [];
43838
43898
  let current = "";
@@ -44074,7 +44134,7 @@ function getBlockServerId(block) {
44074
44134
  return prefix ? prefix.slice(0, -1) : "*";
44075
44135
  }
44076
44136
 
44077
- // ../packages/shared-fbe/src/permissionUtils.ts
44137
+ // ../../../home/hm/repo/todoforai/packages/shared-fbe/src/permissionUtils.ts
44078
44138
  function parsePattern(pattern) {
44079
44139
  const colonIndex = pattern.indexOf(":");
44080
44140
  if (colonIndex === -1)
@@ -44175,7 +44235,7 @@ function getBlockNewPatterns(block, permissions) {
44175
44235
  }
44176
44236
  return pairs.filter(({ raw, pattern }) => !isPatternAllowed(permissions, pattern) && !allow.some((rule) => bashRuleMatchesCmd(rule, serverId, raw))).map(({ pattern }) => pattern);
44177
44237
  }
44178
- // ../packages/shared-fbe/src/systemAgents.ts
44238
+ // ../../../home/hm/repo/todoforai/packages/shared-fbe/src/systemAgents.ts
44179
44239
  var BUSINESS_ANALYZER_AGENT = {
44180
44240
  id: "00000000-0000-0000-0000-202601013151",
44181
44241
  name: "Business Analyzer",
@@ -44321,7 +44381,7 @@ var SYSTEM_AGENTS = {
44321
44381
  [ONBOARDING_AGENT.id]: ONBOARDING_AGENT
44322
44382
  };
44323
44383
  var RECOMMEND_STEP = "Recommend growth TODOs for this project: materialize the ones you genuinely believe fit " + "as proposal cards on the board (api_recommend_template, one call per recommendation). " + "No quota — fewer good cards beat padding.";
44324
- // ../packages/shared-fbe/src/outputLimits.ts
44384
+ // ../../../home/hm/repo/todoforai/packages/shared-fbe/src/outputLimits.ts
44325
44385
  var MAX_LINE_LEN = 300;
44326
44386
  var STREAM_FIRST = 1e4;
44327
44387
  var STREAM_LAST = 1e4;
@@ -44332,7 +44392,7 @@ var OUTPUT_POLICIES = {
44332
44392
  full: { firstLimit: Infinity, lastLimit: 0, hardCap: RUN_OUTPUT_CAP, lineLimit: MAX_LINE_LEN },
44333
44393
  raw: { firstLimit: Infinity, lastLimit: 0, hardCap: Infinity, lineLimit: Infinity }
44334
44394
  };
44335
- // ../packages/shared-fbe/src/model_promos.json
44395
+ // ../../../home/hm/repo/todoforai/packages/shared-fbe/src/model_promos.json
44336
44396
  var model_promos_default = {
44337
44397
  _doc: [
44338
44398
  "Single source of truth for TODOforAI promotional model discounts.",
@@ -44374,10 +44434,10 @@ var model_promos_default = {
44374
44434
  ]
44375
44435
  };
44376
44436
 
44377
- // ../packages/shared-fbe/src/modelPromos.ts
44437
+ // ../../../home/hm/repo/todoforai/packages/shared-fbe/src/modelPromos.ts
44378
44438
  var BOOST_FACTOR = model_promos_default.boost_remaining_price_factor;
44379
44439
  var MODEL_PROMOS = model_promos_default.promos;
44380
- // ../packages/shared-fbe/src/realtime/topics.ts
44440
+ // ../../../home/hm/repo/todoforai/packages/shared-fbe/src/realtime/topics.ts
44381
44441
  var TOPICS = {
44382
44442
  ["todo:new" /* NEW_TODO */]: {
44383
44443
  channel: (p) => SERVER_TO_FRONTENDS.project.new_todo(p.projectId, p.privateOwnerId),
@@ -44483,6 +44543,10 @@ var TOPICS = {
44483
44543
  channel: (p) => SERVER_TO_FRONTENDS.todo.message_removed(p.todoId),
44484
44544
  audience: "frontend"
44485
44545
  },
44546
+ ["todo:comment_update" /* TODO_COMMENT_UPDATE */]: {
44547
+ channel: (p) => SERVER_TO_FRONTENDS.todo.commentUpdate(p.todoId),
44548
+ audience: "frontend"
44549
+ },
44486
44550
  ["todo:msg_meta_usr" /* TODO_MSG_META_USR */]: {
44487
44551
  channel: (p) => SERVER_TO_FRONTENDS.todo.msgMetaUsr(p.todoId),
44488
44552
  audience: "frontend"
@@ -44540,12 +44604,12 @@ var TOPICS = {
44540
44604
  audience: "frontend"
44541
44605
  }
44542
44606
  };
44543
- // ../packages/shared-fbe/src/modelNames.ts
44607
+ // ../../../home/hm/repo/todoforai/packages/shared-fbe/src/modelNames.ts
44544
44608
  var LEGACY_MODEL_MAPPING = {
44545
44609
  claude: DEFAULT_MODEL.slice(DEFAULT_MODEL.indexOf(":") + 1),
44546
44610
  "gemini-pro": "google/gemini-3.1-pro-preview"
44547
44611
  };
44548
- // ../packages/shared-credentials/src/index.ts
44612
+ // ../../../home/hm/repo/todoforai/packages/shared-credentials/src/index.ts
44549
44613
  function normalizeApiUrl2(url) {
44550
44614
  if (url.startsWith("localhost"))
44551
44615
  return `http://${url}`;
@@ -44559,7 +44623,7 @@ import { parseArgs } from "util";
44559
44623
  // package.json
44560
44624
  var package_default = {
44561
44625
  name: "@todoforai/cli",
44562
- version: "0.1.54",
44626
+ version: "0.1.55",
44563
44627
  type: "module",
44564
44628
  bin: {
44565
44629
  "todoforai-cli": "bin/todoforai-cli.js",
@@ -44627,6 +44691,9 @@ Examples:
44627
44691
  tfa-cli show list|rm … # List / take down shown files
44628
44692
  tfa-cli open <url> [todo-id] # Live url preview in the chat
44629
44693
  tfa-cli recommend --template <id> # Add a template as a recommendation card
44694
+ tfa-cli inbox [needs-you|messages|activity] [--unread] # Read the notification feed (the bell)
44695
+ tfa-cli inbox seen [tab] # Mark notifications read
44696
+ tfa-cli notify "Title" "message" [--href /t/<id>] # Note to YOUR OWN user only (Messages tab + phone push); no --to
44630
44697
  tfa-cli claim mint --seed <projectId> [--emails a@x,b@y] [--ttl <sec>] # Mint /claim/<token> links for a project you own
44631
44698
  tfa-cli next [--direction "<text>"] # Analyzer growth recommendation cards
44632
44699
  tfa-cli <command> help # Details per command
@@ -44707,7 +44774,7 @@ var WORD_FLAGS = {
44707
44774
  agents: "list-agents",
44708
44775
  config: "show-config"
44709
44776
  };
44710
- var HELP_SUBCOMMANDS = ["agent", "todo", "project", "brand", "device", "list", "ls", "status", "show", "open"];
44777
+ var HELP_SUBCOMMANDS = ["agent", "todo", "project", "brand", "device", "list", "ls", "status", "show", "open", "inbox", "notify"];
44711
44778
  function parseCliArgs() {
44712
44779
  const { values, positionals } = parseArgs({
44713
44780
  args: process.argv.slice(2),
@@ -44742,6 +44809,10 @@ function parseCliArgs() {
44742
44809
  seed: { type: "string" },
44743
44810
  emails: { type: "string" },
44744
44811
  ttl: { type: "string" },
44812
+ subject: { type: "string" },
44813
+ href: { type: "string" },
44814
+ unread: { type: "boolean", default: false },
44815
+ limit: { type: "string" },
44745
44816
  resume: { type: "string", short: "r" },
44746
44817
  continue: { type: "boolean", short: "c", default: false },
44747
44818
  "non-interactive": { type: "boolean", short: "n", default: false },
@@ -46999,7 +47070,88 @@ function renderDiff(originalContent, modifiedContent, filePath) {
46999
47070
  return parts.join("");
47000
47071
  }
47001
47072
 
47073
+ // src/shutdown.ts
47074
+ var activeRun = null;
47075
+ var cleanups = [];
47076
+ var exiting = null;
47077
+ var cancelling = null;
47078
+ var CANCEL_TIMEOUT_MS = 5000;
47079
+ function setActiveRun(run) {
47080
+ activeRun = run;
47081
+ }
47082
+ function withTimeout(p, ms, fallback) {
47083
+ let timer;
47084
+ const timeout = new Promise((r) => {
47085
+ timer = setTimeout(() => r(fallback), ms);
47086
+ });
47087
+ return Promise.race([p, timeout]).finally(() => clearTimeout(timer));
47088
+ }
47089
+ function cancelActiveRun(timeoutMs = CANCEL_TIMEOUT_MS) {
47090
+ if (cancelling)
47091
+ return cancelling;
47092
+ const pending = activeRun;
47093
+ if (!pending)
47094
+ return Promise.resolve({ ok: false, todoId: null });
47095
+ const res = { ok: false };
47096
+ const attempt = (async () => {
47097
+ const run = await pending;
47098
+ res.todoId = run?.todoId ?? null;
47099
+ if (!run)
47100
+ return res;
47101
+ if (!run.ws.connected && !await run.ws.connect())
47102
+ return res;
47103
+ res.ok = await run.ws.sendInterrupt(run.projectId, run.todoId);
47104
+ return res;
47105
+ })().catch(() => res);
47106
+ cancelling = withTimeout(attempt, timeoutMs, res).finally(() => {
47107
+ if (activeRun === pending)
47108
+ activeRun = null;
47109
+ cancelling = null;
47110
+ });
47111
+ return cancelling;
47112
+ }
47113
+ function shutdown(code, reason) {
47114
+ return exiting ??= (async () => {
47115
+ if (reason)
47116
+ process.stderr.write(`
47117
+ ${reason}
47118
+ `);
47119
+ if (activeRun) {
47120
+ const { ok, todoId } = await cancelActiveRun();
47121
+ if (ok)
47122
+ process.stderr.write(`Stopped todo ${todoId}
47123
+ `);
47124
+ else if (todoId)
47125
+ process.stderr.write(`Warning: could not stop todo ${todoId} (backend unreachable)
47126
+ `);
47127
+ else if (todoId === undefined)
47128
+ process.stderr.write(`Warning: could not stop the todo being created (timed out)
47129
+ `);
47130
+ }
47131
+ for (const fn of cleanups.splice(0)) {
47132
+ try {
47133
+ fn();
47134
+ } catch {}
47135
+ }
47136
+ process.exit(code);
47137
+ })();
47138
+ }
47139
+
47002
47140
  // src/watch.ts
47141
+ var RECONNECT_WINDOW_MS = 60000;
47142
+ var DISCONNECTED = { type: "socket:closed", success: false };
47143
+ async function resubscribe(ws, todoId, callback, windowMs = RECONNECT_WINDOW_MS) {
47144
+ const deadline = Date.now() + windowMs;
47145
+ for (let delay = 1000;; delay = Math.min(delay * 2, 5000)) {
47146
+ await new Promise((r) => setTimeout(r, Math.max(0, Math.min(delay, deadline - Date.now()))));
47147
+ const left = deadline - Date.now();
47148
+ if (left <= 0)
47149
+ return false;
47150
+ const attempt = ws.subscribe(todoId, callback, { replayStatus: true }).catch(() => false);
47151
+ if (await withTimeout(attempt, left, false))
47152
+ return true;
47153
+ }
47154
+ }
47003
47155
  var diffStoreByWs = new WeakMap;
47004
47156
  function classifyBlock(info) {
47005
47157
  const inner = (info.block_type || "").toLowerCase();
@@ -47081,12 +47233,8 @@ async function watchTodo(ws, todoId, projectId, opts = {}) {
47081
47233
  process.removeAllListeners("SIGINT");
47082
47234
  process.on("SIGINT", () => {
47083
47235
  interruptCount++;
47084
- if (interruptCount >= 2) {
47085
- process.stderr.write(`
47086
- ${RED}Force exit (double Ctrl+C)${RESET}
47087
- `);
47088
- process.exit(130);
47089
- }
47236
+ if (opts.exitOnInterrupt || interruptCount >= 2)
47237
+ return void shutdown(130, "Cancelled by user (Ctrl+C)");
47090
47238
  process.stderr.write(`
47091
47239
  ${YELLOW}Interrupting... (Ctrl+C again to force exit)${RESET}
47092
47240
  `);
@@ -47285,8 +47433,22 @@ ${DIM}[todo:status] ${status}${RESET}
47285
47433
  callback(msgType, payload);
47286
47434
  }
47287
47435
  }
47436
+ setActiveRun({ ws, projectId, todoId });
47288
47437
  try {
47289
- const result = await ws.waitForCompletion(todoId, callback);
47438
+ const subscribed = await ws.subscribe(todoId, callback).catch(() => false);
47439
+ let result = subscribed ? await ws.completion(todoId) : DISCONNECTED;
47440
+ while (result?.type === "socket:closed") {
47441
+ process.stderr.write(`
47442
+ ${YELLOW}Connection lost — reconnecting...${RESET}
47443
+ `);
47444
+ if (!await resubscribe(ws, todoId, callback)) {
47445
+ await shutdown(1, `${RED}Error: backend unreachable for ${RECONNECT_WINDOW_MS / 1000}s — giving up on todo ${todoId}${RESET}`);
47446
+ }
47447
+ process.stderr.write(`${DIM}Reconnected.${RESET}
47448
+ `);
47449
+ result = await ws.completion(todoId);
47450
+ }
47451
+ setActiveRun(null);
47290
47452
  process.stdout.write(`
47291
47453
  `);
47292
47454
  if (!result?.success) {
@@ -47299,6 +47461,8 @@ ${DIM}[todo:status] ${status}${RESET}
47299
47461
  }
47300
47462
  return true;
47301
47463
  } catch (e) {
47464
+ await cancelActiveRun();
47465
+ process.exitCode = 1;
47302
47466
  if (!opts.suppressCancelNotice) {
47303
47467
  process.stderr.write(`${YELLOW}Interrupted${RESET}
47304
47468
  `);
@@ -48569,6 +48733,425 @@ async function listTodosCommand(api, defaultProjectId, argv) {
48569
48733
  `);
48570
48734
  }
48571
48735
 
48736
+ // src/notify-command.ts
48737
+ import { createHash } from "crypto";
48738
+
48739
+ // src/input.ts
48740
+ function readMultiline2(prompt, history) {
48741
+ let cancelFn = () => {};
48742
+ const promise = new Promise((resolve4, reject) => {
48743
+ const out = process.stderr;
48744
+ let buf = "";
48745
+ let cursor = 0;
48746
+ let pasting = false;
48747
+ let done = false;
48748
+ let screenRow = 0;
48749
+ let historyIndex = -1;
48750
+ let savedCurrent = "";
48751
+ const promptLen = prompt.replace(/\x1b\[[0-9;]*m/g, "").length;
48752
+ out.write(prompt);
48753
+ process.stdin.setRawMode(true);
48754
+ process.stdin.resume();
48755
+ out.write("\x1B[?2004h");
48756
+ function rowOf(pos) {
48757
+ const cols = process.stderr.columns || 80;
48758
+ const lines = buf.slice(0, pos).split(`
48759
+ `);
48760
+ let row = 0;
48761
+ for (let i = 0;i < lines.length; i++) {
48762
+ const len = (i === 0 ? promptLen : 0) + lines[i].length;
48763
+ if (i < lines.length - 1) {
48764
+ row += len === 0 ? 1 : Math.ceil(len / cols);
48765
+ } else {
48766
+ row += len > 0 ? Math.ceil(len / cols) - 1 : 0;
48767
+ }
48768
+ }
48769
+ return row;
48770
+ }
48771
+ function colOf(pos) {
48772
+ const cols = process.stderr.columns || 80;
48773
+ const lastNl = buf.lastIndexOf(`
48774
+ `, pos - 1);
48775
+ const lineStart = lastNl + 1;
48776
+ const offset = lineStart === 0 ? promptLen : 0;
48777
+ const linePos = offset + (pos - lineStart);
48778
+ if (linePos > 0 && linePos % cols === 0)
48779
+ return cols;
48780
+ return linePos % cols;
48781
+ }
48782
+ function totalRows() {
48783
+ const cols = process.stderr.columns || 80;
48784
+ const lines = buf.split(`
48785
+ `);
48786
+ let rows = 0;
48787
+ for (let i = 0;i < lines.length; i++) {
48788
+ const len = (i === 0 ? promptLen : 0) + lines[i].length;
48789
+ rows += len === 0 ? 1 : Math.ceil(len / cols);
48790
+ }
48791
+ return rows;
48792
+ }
48793
+ function redraw() {
48794
+ const cols = process.stderr.columns || 80;
48795
+ out.write("\r");
48796
+ if (screenRow > 0)
48797
+ out.write(`\x1B[${screenRow}A`);
48798
+ out.write("\x1B[J");
48799
+ out.write(prompt + buf.replace(/\n/g, `\r
48800
+ `));
48801
+ const targetRow = rowOf(cursor);
48802
+ const endRow = totalRows() - 1;
48803
+ const rowsBack = endRow - targetRow;
48804
+ const col = colOf(cursor);
48805
+ if (rowsBack > 0)
48806
+ out.write(`\x1B[${rowsBack}A`);
48807
+ if (col <= cols) {
48808
+ out.write("\r");
48809
+ if (col > 0 && col < cols)
48810
+ out.write(`\x1B[${col}C`);
48811
+ if (col === cols)
48812
+ out.write(`\x1B[${cols}G`);
48813
+ }
48814
+ screenRow = targetRow;
48815
+ }
48816
+ function finish(cancelled) {
48817
+ if (done)
48818
+ return;
48819
+ done = true;
48820
+ const rowsDown = totalRows() - 1 - screenRow;
48821
+ if (rowsDown > 0)
48822
+ out.write(`\x1B[${rowsDown}B`);
48823
+ out.write("\x1B[?2004l");
48824
+ out.write(`
48825
+ `);
48826
+ process.stdin.setRawMode(false);
48827
+ process.stdin.pause();
48828
+ process.stdin.removeListener("data", onData);
48829
+ if (cancelled)
48830
+ reject(new Error("cancelled"));
48831
+ else
48832
+ resolve4(buf.trim());
48833
+ }
48834
+ function wordRight() {
48835
+ let p = cursor;
48836
+ while (p < buf.length && buf[p] === " ")
48837
+ p++;
48838
+ while (p < buf.length && buf[p] !== " ")
48839
+ p++;
48840
+ return p;
48841
+ }
48842
+ function wordLeft() {
48843
+ let p = cursor;
48844
+ while (p > 0 && buf[p - 1] === " ")
48845
+ p--;
48846
+ while (p > 0 && buf[p - 1] !== " ")
48847
+ p--;
48848
+ return p;
48849
+ }
48850
+ function killToEnd() {
48851
+ buf = buf.slice(0, cursor);
48852
+ redraw();
48853
+ }
48854
+ function deleteWordBack() {
48855
+ const to = wordLeft();
48856
+ if (to === cursor)
48857
+ return;
48858
+ buf = buf.slice(0, to) + buf.slice(cursor);
48859
+ cursor = to;
48860
+ redraw();
48861
+ }
48862
+ function handleCSI(chunk, start) {
48863
+ let i = start + 1;
48864
+ if (i >= chunk.length || chunk[i] !== "[") {
48865
+ if (i < chunk.length)
48866
+ i++;
48867
+ return i;
48868
+ }
48869
+ i++;
48870
+ let params = "";
48871
+ while (i < chunk.length && /[0-9;]/.test(chunk[i])) {
48872
+ params += chunk[i];
48873
+ i++;
48874
+ }
48875
+ const final = i < chunk.length ? chunk[i] : "";
48876
+ i++;
48877
+ const parts = params.split(";");
48878
+ const modifier = parts.length > 1 ? parseInt(parts[1]) : 0;
48879
+ const code = parts[0] || "";
48880
+ const ctrl = modifier === 5;
48881
+ switch (final) {
48882
+ case "D":
48883
+ if (ctrl) {
48884
+ cursor = wordLeft();
48885
+ redraw();
48886
+ } else if (cursor > 0) {
48887
+ cursor--;
48888
+ redraw();
48889
+ }
48890
+ break;
48891
+ case "C":
48892
+ if (ctrl) {
48893
+ cursor = wordRight();
48894
+ redraw();
48895
+ } else if (cursor < buf.length) {
48896
+ cursor++;
48897
+ redraw();
48898
+ }
48899
+ break;
48900
+ case "A":
48901
+ if (history && history.length > 0) {
48902
+ if (historyIndex === -1)
48903
+ savedCurrent = buf;
48904
+ if (historyIndex < history.length - 1) {
48905
+ historyIndex++;
48906
+ buf = history[history.length - 1 - historyIndex];
48907
+ cursor = buf.length;
48908
+ redraw();
48909
+ }
48910
+ }
48911
+ break;
48912
+ case "B":
48913
+ if (history && historyIndex >= 0) {
48914
+ historyIndex--;
48915
+ if (historyIndex === -1) {
48916
+ buf = savedCurrent;
48917
+ } else {
48918
+ buf = history[history.length - 1 - historyIndex];
48919
+ }
48920
+ cursor = buf.length;
48921
+ redraw();
48922
+ }
48923
+ break;
48924
+ case "H":
48925
+ cursor = 0;
48926
+ redraw();
48927
+ break;
48928
+ case "F":
48929
+ cursor = buf.length;
48930
+ redraw();
48931
+ break;
48932
+ case "~":
48933
+ if (code === "3" && cursor < buf.length) {
48934
+ buf = buf.slice(0, cursor) + buf.slice(cursor + 1);
48935
+ redraw();
48936
+ }
48937
+ break;
48938
+ }
48939
+ return i;
48940
+ }
48941
+ function onData(data) {
48942
+ let s = data.toString("utf-8");
48943
+ while (s.length > 0 && !done) {
48944
+ if (pasting) {
48945
+ const end = s.indexOf("\x1B[201~");
48946
+ if (end >= 0) {
48947
+ const text = s.slice(0, end).replace(/\r\n?|\n/g, `
48948
+ `);
48949
+ buf = buf.slice(0, cursor) + text + buf.slice(cursor);
48950
+ cursor += text.length;
48951
+ pasting = false;
48952
+ s = s.slice(end + 6);
48953
+ redraw();
48954
+ } else {
48955
+ const text = s.replace(/\r\n?|\n/g, `
48956
+ `);
48957
+ buf = buf.slice(0, cursor) + text + buf.slice(cursor);
48958
+ cursor += text.length;
48959
+ s = "";
48960
+ redraw();
48961
+ }
48962
+ } else {
48963
+ const ps = s.indexOf("\x1B[200~");
48964
+ const chunk = ps >= 0 ? s.slice(0, ps) : s;
48965
+ for (let i = 0;i < chunk.length && !done; i++) {
48966
+ const c3 = chunk.charCodeAt(i);
48967
+ if (c3 === 3) {
48968
+ finish(true);
48969
+ return;
48970
+ }
48971
+ if (c3 === 13 || c3 === 10) {
48972
+ finish(false);
48973
+ return;
48974
+ }
48975
+ if (c3 === 23) {
48976
+ deleteWordBack();
48977
+ } else if (c3 === 11) {
48978
+ killToEnd();
48979
+ } else if (c3 === 127 || c3 === 8) {
48980
+ if (cursor > 0) {
48981
+ buf = buf.slice(0, cursor - 1) + buf.slice(cursor);
48982
+ cursor--;
48983
+ redraw();
48984
+ }
48985
+ } else if (c3 === 27) {
48986
+ if (i + 1 < chunk.length && chunk.charCodeAt(i + 1) === 13) {
48987
+ buf = buf.slice(0, cursor) + `
48988
+ ` + buf.slice(cursor);
48989
+ cursor++;
48990
+ i++;
48991
+ redraw();
48992
+ } else {
48993
+ i = handleCSI(chunk, i) - 1;
48994
+ }
48995
+ } else if (c3 === 1) {
48996
+ cursor = 0;
48997
+ redraw();
48998
+ } else if (c3 === 5) {
48999
+ cursor = buf.length;
49000
+ redraw();
49001
+ } else if (c3 >= 32) {
49002
+ buf = buf.slice(0, cursor) + chunk[i] + buf.slice(cursor);
49003
+ cursor++;
49004
+ redraw();
49005
+ }
49006
+ }
49007
+ if (ps >= 0) {
49008
+ pasting = true;
49009
+ s = s.slice(ps + 6);
49010
+ } else
49011
+ s = "";
49012
+ }
49013
+ }
49014
+ }
49015
+ process.stdin.on("data", onData);
49016
+ cancelFn = () => finish(true);
49017
+ });
49018
+ return { promise, cancel: () => cancelFn() };
49019
+ }
49020
+ async function readStdin2() {
49021
+ if (process.stdin.isTTY) {
49022
+ try {
49023
+ const content2 = await readMultiline2(`${BRIGHT_WHITE}TODO>${RESET} `).promise;
49024
+ if (!content2) {
49025
+ process.stderr.write(`Error: Empty input
49026
+ `);
49027
+ process.exit(1);
49028
+ }
49029
+ return content2;
49030
+ } catch {
49031
+ process.stderr.write(`
49032
+ Cancelled
49033
+ `);
49034
+ process.exit(1);
49035
+ }
49036
+ }
49037
+ const chunks = [];
49038
+ for await (const chunk of process.stdin)
49039
+ chunks.push(chunk);
49040
+ const content = Buffer.concat(chunks).toString("utf-8").trim();
49041
+ if (!content) {
49042
+ process.stderr.write(`Error: Empty input
49043
+ `);
49044
+ process.exit(1);
49045
+ }
49046
+ return content;
49047
+ }
49048
+
49049
+ // src/notify-command.ts
49050
+ var TIERS = { "needs-you": "priority", priority: "priority", activity: "activity", messages: "user_messages" };
49051
+ function printInboxHelp() {
49052
+ process.stderr.write(`
49053
+ tfa-cli inbox — the user's notification feed (the bell)
49054
+
49055
+ Usage:
49056
+ tfa-cli inbox [needs-you|messages|activity] [--unread] [--limit 20] [--json]
49057
+ tfa-cli inbox seen [needs-you|messages|activity] Mark read (omit = everything)
49058
+
49059
+ Filters (same as the bell's chips):
49060
+ needs-you invites, offers, a todo waiting on the user, a failed todo (alias: priority)
49061
+ messages notes from the TODO for AI team or from an agent
49062
+ activity finished / recommended todos
49063
+ `);
49064
+ }
49065
+ function printNotifyHelp() {
49066
+ process.stderr.write(`
49067
+ tfa-cli notify — leave a note for YOUR user (their Messages tab + phone push)
49068
+
49069
+ Usage:
49070
+ tfa-cli notify "Title" "message" [--href /t/<todo-id>] [--subject <key>] [--json]
49071
+ echo "message" | tfa-cli notify "Title"
49072
+
49073
+ Recipient: ALWAYS the user this key belongs to (the person you work for). There is no --to:
49074
+ it cannot message a teammate, a customer or any other account. Shown as from your agent.
49075
+ --subject dedupes: the same subject is delivered once (default: hash of title+message,
49076
+ so a retry never double-notifies). --href must be an in-app path. Max 10 new notes/day.
49077
+ `);
49078
+ }
49079
+ function tierArg(name) {
49080
+ if (!name)
49081
+ return;
49082
+ const tier = TIERS[name];
49083
+ if (!tier) {
49084
+ process.stderr.write(`${RED}Unknown tab "${name}" — use needs-you, messages, activity${RESET}
49085
+ `);
49086
+ process.exit(2);
49087
+ }
49088
+ return tier;
49089
+ }
49090
+ var when = (ms) => new Date(ms).toLocaleString(undefined, { dateStyle: "short", timeStyle: "short" });
49091
+ async function inboxCommand(api, positionals, args) {
49092
+ const [, first, second] = positionals;
49093
+ if (first === "seen") {
49094
+ const tier2 = tierArg(second);
49095
+ await api.markFeedSeen(tier2 ? { tier: tier2 } : {});
49096
+ process.stderr.write(`${GREEN}✅ Marked ${second ?? "everything"} read${RESET}
49097
+ `);
49098
+ return;
49099
+ }
49100
+ const tier = tierArg(first);
49101
+ const limit = args.limit ? Number(args.limit) : 20;
49102
+ const page = await api.listFeed({ tier, limit, sinceSeen: !!args.unread || undefined });
49103
+ if (args.json) {
49104
+ console.log(JSON.stringify(page));
49105
+ return;
49106
+ }
49107
+ const counts = page.unseenByTier;
49108
+ if (counts)
49109
+ process.stderr.write(`${DIM}unread — needs you ${counts.priority} · messages ${counts.user_messages} · activity ${counts.activity}${RESET}
49110
+ `);
49111
+ if (!page.items.length) {
49112
+ process.stderr.write(`${DIM}Nothing here${RESET}
49113
+ `);
49114
+ return;
49115
+ }
49116
+ for (const e of page.items) {
49117
+ const from = e.sender ? `${CYAN}${senderLabel(e.sender)}${RESET} ` : "";
49118
+ const dot = e.unread ? `${YELLOW}●${RESET}` : " ";
49119
+ const state = e.resolution ? ` ${DIM}[${e.resolution}]${RESET}` : "";
49120
+ console.log(`${dot} ${DIM}${when(e.createdAt)}${RESET} ${from}${BOLD}${e.title ?? e.message}${RESET}${state}`);
49121
+ if (e.title && e.message)
49122
+ console.log(` ${e.message.replace(/\n/g, `
49123
+ `)}`);
49124
+ if (e.href)
49125
+ console.log(` ${DIM}${e.href}${RESET}`);
49126
+ }
49127
+ }
49128
+ var RECIPIENT_FLAGS = ["--to", "--email", "--user", "--recipient", "--cc"];
49129
+ async function notifyCommand(api, positionals, args) {
49130
+ const argv = process.argv.slice(2);
49131
+ const bad = argv.find((a) => RECIPIENT_FLAGS.some((f) => a === f || a.startsWith(`${f}=`)));
49132
+ if (bad) {
49133
+ process.stderr.write(`${RED}notify has no recipient option (${bad.split("=")[0]}): it only reaches the user this key belongs to — your own user. Messaging other people is not supported.${RESET}
49134
+ `);
49135
+ process.exit(2);
49136
+ }
49137
+ const [, title, ...rest] = positionals;
49138
+ const message = rest.join(" ") || (process.stdin.isTTY ? "" : (await readStdin2()).trim());
49139
+ if (!title || !message) {
49140
+ printNotifyHelp();
49141
+ process.exit(2);
49142
+ }
49143
+ const subject = args.subject || createHash("sha256").update(`${title}
49144
+ ${message}`).digest("hex").slice(0, 16);
49145
+ const row = await api.sendFeedMessage({ subject, title, message, ...args.href ? { href: String(args.href) } : {} });
49146
+ if (args.json) {
49147
+ console.log(JSON.stringify(row));
49148
+ return;
49149
+ }
49150
+ process.stderr.write(row.created !== false ? `${GREEN}✅ Delivered to your own user's Messages inbox (not anyone else)${RESET} ${DIM}(subject ${subject})${RESET}
49151
+ ` : `${DIM}Already sent (subject ${subject}) — nothing new delivered${RESET}
49152
+ `);
49153
+ }
49154
+
48572
49155
  // src/index.ts
48573
49156
  import { randomUUID as randomUUID3 } from "crypto";
48574
49157
 
@@ -48833,7 +49416,7 @@ async function spawnMayflyBridge(apiUrl, todoId, workspace, opts = {}) {
48833
49416
  }
48834
49417
  });
48835
49418
  });
48836
- return { child, stop };
49419
+ return { child, stop, stopped: () => stopped };
48837
49420
  }
48838
49421
 
48839
49422
  // src/acp.ts
@@ -72898,6 +73481,93 @@ async function runAcp(apiUrl, apiKey, opts) {
72898
73481
  await agent2.close();
72899
73482
  }
72900
73483
 
73484
+ // src/shutdown.ts
73485
+ var activeRun2 = null;
73486
+ var cleanups2 = [];
73487
+ var exiting2 = null;
73488
+ var cancelling2 = null;
73489
+ var CANCEL_TIMEOUT_MS2 = 5000;
73490
+ function setActiveRun2(run2) {
73491
+ activeRun2 = run2;
73492
+ }
73493
+ function trackStart(ws, projectId, started) {
73494
+ setActiveRun2(started.then((todoId) => ({ ws, projectId, todoId }), () => null));
73495
+ }
73496
+ function onShutdown(fn) {
73497
+ cleanups2.push(fn);
73498
+ }
73499
+ function withTimeout2(p, ms, fallback) {
73500
+ let timer;
73501
+ const timeout = new Promise((r) => {
73502
+ timer = setTimeout(() => r(fallback), ms);
73503
+ });
73504
+ return Promise.race([p, timeout]).finally(() => clearTimeout(timer));
73505
+ }
73506
+ function cancelActiveRun2(timeoutMs = CANCEL_TIMEOUT_MS2) {
73507
+ if (cancelling2)
73508
+ return cancelling2;
73509
+ const pending = activeRun2;
73510
+ if (!pending)
73511
+ return Promise.resolve({ ok: false, todoId: null });
73512
+ const res = { ok: false };
73513
+ const attempt = (async () => {
73514
+ const run2 = await pending;
73515
+ res.todoId = run2?.todoId ?? null;
73516
+ if (!run2)
73517
+ return res;
73518
+ if (!run2.ws.connected && !await run2.ws.connect())
73519
+ return res;
73520
+ res.ok = await run2.ws.sendInterrupt(run2.projectId, run2.todoId);
73521
+ return res;
73522
+ })().catch(() => res);
73523
+ cancelling2 = withTimeout2(attempt, timeoutMs, res).finally(() => {
73524
+ if (activeRun2 === pending)
73525
+ activeRun2 = null;
73526
+ cancelling2 = null;
73527
+ });
73528
+ return cancelling2;
73529
+ }
73530
+ function shutdown2(code, reason) {
73531
+ return exiting2 ??= (async () => {
73532
+ if (reason)
73533
+ process.stderr.write(`
73534
+ ${reason}
73535
+ `);
73536
+ if (activeRun2) {
73537
+ const { ok, todoId } = await cancelActiveRun2();
73538
+ if (ok)
73539
+ process.stderr.write(`Stopped todo ${todoId}
73540
+ `);
73541
+ else if (todoId)
73542
+ process.stderr.write(`Warning: could not stop todo ${todoId} (backend unreachable)
73543
+ `);
73544
+ else if (todoId === undefined)
73545
+ process.stderr.write(`Warning: could not stop the todo being created (timed out)
73546
+ `);
73547
+ }
73548
+ for (const fn of cleanups2.splice(0)) {
73549
+ try {
73550
+ fn();
73551
+ } catch {}
73552
+ }
73553
+ process.exit(code);
73554
+ })();
73555
+ }
73556
+ function finish(code) {
73557
+ if (exiting2)
73558
+ return;
73559
+ process.exitCode = code;
73560
+ process.stdout.end(() => {
73561
+ if (!exiting2)
73562
+ process.exit(code);
73563
+ });
73564
+ }
73565
+ function installSignalHandlers() {
73566
+ process.on("SIGINT", () => void shutdown2(130, "Cancelled by user (Ctrl+C)"));
73567
+ process.on("SIGTERM", () => void shutdown2(143, "Terminated (SIGTERM)"));
73568
+ process.on("SIGHUP", () => void shutdown2(129, "Hangup (SIGHUP)"));
73569
+ }
73570
+
72901
73571
  // src/index.ts
72902
73572
  try {
72903
73573
  const pkgPath = path4.resolve(fileURLToPath(import.meta.url), "../../package.json");
@@ -72973,7 +73643,7 @@ async function interactiveLoop(ws, api2, todoId, projectId, agent2, json2, autoA
72973
73643
  cfg.addToHistory(input2);
72974
73644
  process.stderr.write("\u2500".repeat(40) + `
72975
73645
  `);
72976
- await api2.addMessage(projectId, input2, agent2, todoId);
73646
+ await appendTracked(api2, ws, projectId, input2, agent2, todoId);
72977
73647
  await watchTodo(ws, todoId, projectId, {
72978
73648
  json: json2,
72979
73649
  autoApprove,
@@ -72984,6 +73654,11 @@ async function interactiveLoop(ws, api2, todoId, projectId, agent2, json2, autoA
72984
73654
  }
72985
73655
  }
72986
73656
  }
73657
+ async function appendTracked(api2, ws, projectId, text, agent2, todoId) {
73658
+ const sent = api2.addMessage(projectId, text, agent2, todoId).then(() => todoId);
73659
+ trackStart(ws, projectId, sent);
73660
+ await sent;
73661
+ }
72987
73662
  async function linkToSpawningBlock(api2, todoId) {
72988
73663
  const { TODOFORAI_TODO_ID: pTodo, TODOFORAI_MESSAGE_ID: pMsg, TODOFORAI_BLOCK_ID: pBlock } = process.env;
72989
73664
  if (!pTodo || !pMsg || !pBlock || pTodo === todoId)
@@ -72992,12 +73667,7 @@ async function linkToSpawningBlock(api2, todoId) {
72992
73667
  `));
72993
73668
  }
72994
73669
  async function main() {
72995
- process.on("SIGINT", () => {
72996
- process.stderr.write(`
72997
- Cancelled by user (Ctrl+C)
72998
- `);
72999
- process.exit(130);
73000
- });
73670
+ installSignalHandlers();
73001
73671
  const { values: args, positionals } = parseCliArgs();
73002
73672
  const groupTag = args.group === undefined ? getEnv("GROUP_ID") : String(args.group);
73003
73673
  const groupName = args["group-name"] === undefined ? undefined : String(args["group-name"]);
@@ -73042,11 +73712,19 @@ Cancelled by user (Ctrl+C)
73042
73712
  printDeviceHelp();
73043
73713
  process.exit(0);
73044
73714
  }
73715
+ if (positionals[0] === "inbox" && args.help) {
73716
+ printInboxHelp();
73717
+ process.exit(0);
73718
+ }
73719
+ if (positionals[0] === "notify" && args.help) {
73720
+ printNotifyHelp();
73721
+ process.exit(0);
73722
+ }
73045
73723
  if ((positionals[0] === "list" || positionals[0] === "ls") && args.help) {
73046
73724
  printListTodosHelp();
73047
73725
  process.exit(0);
73048
73726
  }
73049
- if (args.help && !["list", "ls", "agent", "todo", "project", "brand", "device"].includes(positionals[0])) {
73727
+ if (args.help && !["list", "ls", "agent", "todo", "project", "brand", "device", "inbox", "notify"].includes(positionals[0])) {
73050
73728
  printUsage();
73051
73729
  process.exit(0);
73052
73730
  }
@@ -73177,6 +73855,14 @@ Cancelled by user (Ctrl+C)
73177
73855
  await deviceCommand(api2, positionals, args);
73178
73856
  return;
73179
73857
  }
73858
+ if (positionals[0] === "inbox") {
73859
+ await inboxCommand(api2, positionals, args);
73860
+ return;
73861
+ }
73862
+ if (positionals[0] === "notify") {
73863
+ await notifyCommand(api2, positionals, args);
73864
+ return;
73865
+ }
73180
73866
  if (positionals[0] === "delete") {
73181
73867
  const todoId = positionals[1];
73182
73868
  if (!todoId) {
@@ -73533,6 +74219,7 @@ Cancelled by user (Ctrl+C)
73533
74219
  }
73534
74220
  if (!args["no-watch"]) {
73535
74221
  const ws2 = new FrontendWebSocket(apiUrl, apiKey);
74222
+ setActiveRun2({ ws: ws2, projectId: projectId2, todoId });
73536
74223
  await ws2.connect();
73537
74224
  const autoApprove = !!args["dangerously-skip-permissions"];
73538
74225
  let agent3 = todo2.agentSettings || { id: todo2.agentSettingsId };
@@ -73543,7 +74230,8 @@ Cancelled by user (Ctrl+C)
73543
74230
  await watchTodo(ws2, todoId, projectId2, {
73544
74231
  json: !!args.json,
73545
74232
  autoApprove,
73546
- agentSettings: agent3
74233
+ agentSettings: agent3,
74234
+ exitOnInterrupt: !!args["non-interactive"]
73547
74235
  });
73548
74236
  if (!args["non-interactive"]) {
73549
74237
  process.stderr.write(`
@@ -73592,9 +74280,9 @@ Resumed: ${CYAN2}${getFrontendUrl(apiUrl, todoId)}${RESET2}
73592
74280
  const followUp = promptArg(positionals) ?? (process.stdin.isTTY ? "" : await readStdin());
73593
74281
  if (followUp) {
73594
74282
  cfg.addToHistory(followUp);
73595
- await api2.addMessage(projectId2, followUp, agent3, todoId);
74283
+ await appendTracked(api2, ws2, projectId2, followUp, agent3, todoId);
73596
74284
  await linkToSpawningBlock(api2, todoId);
73597
- await watchTodo(ws2, todoId, projectId2, { json: !!args.json, autoApprove, agentSettings: agent3 });
74285
+ await watchTodo(ws2, todoId, projectId2, { json: !!args.json, autoApprove, agentSettings: agent3, exitOnInterrupt: !!args["non-interactive"] });
73598
74286
  }
73599
74287
  if (!args["non-interactive"]) {
73600
74288
  await interactiveLoop(ws2, api2, todoId, projectId2, agent3, !!args.json, autoApprove, cfg);
@@ -73613,9 +74301,15 @@ Resumed: ${CYAN2}${getFrontendUrl(apiUrl, todoId)}${RESET2}
73613
74301
  const bridgeReady = !args["no-bridge"] && !args["no-watch"] && !args.isolated ? ensureBridgeRunning2(apiUrl, apiKey) : null;
73614
74302
  const isolatedTodoId = args.isolated ? randomUUID3() : undefined;
73615
74303
  const mayfly = isolatedTodoId ? await spawnMayflyBridge(apiUrl, isolatedTodoId, realpathSync2(resolve4(args.path || ".")), { debug: !!args.debug, apiKey }) : null;
73616
- if (mayfly)
74304
+ if (mayfly) {
73617
74305
  process.stderr.write(`${DIM2}Isolated bridge:${RESET2} ${CYAN2}mayfly-${isolatedTodoId}${RESET2}
73618
74306
  `);
74307
+ onShutdown(mayfly.stop);
74308
+ mayfly.child.on("exit", (code, sig) => {
74309
+ if (!mayfly.stopped())
74310
+ shutdown2(1, `${RED3}Error: isolated bridge died (${sig ?? `code ${code}`}) \u2014 stopping todo${RESET2}`);
74311
+ });
74312
+ }
73619
74313
  let preMatchedAgent = null;
73620
74314
  let agents = null;
73621
74315
  if (args.agent) {
@@ -73719,15 +74413,16 @@ Resumed: ${CYAN2}${getFrontendUrl(apiUrl, todoId)}${RESET2}
73719
74413
  agent2 = { ...agent2, permissions: { ...perms, allow: [...perms.allow || [], "*:*"] } };
73720
74414
  }
73721
74415
  cfg.addToHistory(content);
74416
+ const created = api2.addMessage(projectId, content, agent2, isolatedTodoId, undefined, undefined, groupTag || undefined, groupName);
74417
+ if (ws)
74418
+ trackStart(ws, projectId, created.then((t) => t.id));
73722
74419
  let todo;
73723
74420
  try {
73724
- todo = await api2.addMessage(projectId, content, agent2, isolatedTodoId, undefined, undefined, groupTag || undefined, groupName);
74421
+ todo = await created;
73725
74422
  } catch (e) {
73726
74423
  if (!args.project && !envProjectId && cfgScope.data.default_project_id === projectId && /failed: 403/.test(e.message || "")) {
73727
74424
  cfgScope.clearDefaultProject();
73728
- process.stderr.write(`${RED3}Not authorized for cached default project ${projectName} (${projectId}) \u2014 cleared it. Re-run to pick a project.${RESET2}
73729
- `);
73730
- process.exit(1);
74425
+ throw new Error(`Not authorized for cached default project ${projectName} (${projectId}) \u2014 cleared it. Re-run to pick a project.`);
73731
74426
  }
73732
74427
  throw e;
73733
74428
  }
@@ -73748,7 +74443,8 @@ Resumed: ${CYAN2}${getFrontendUrl(apiUrl, todoId)}${RESET2}
73748
74443
  await watchTodo(ws, actualTodoId, projectId, {
73749
74444
  json: !!args.json,
73750
74445
  autoApprove,
73751
- agentSettings: agent2
74446
+ agentSettings: agent2,
74447
+ exitOnInterrupt: !!args["non-interactive"]
73752
74448
  });
73753
74449
  if (!args["non-interactive"]) {
73754
74450
  process.stderr.write(`
@@ -73760,10 +74456,6 @@ ${"\u2500".repeat(40)}
73760
74456
  }
73761
74457
  mayfly?.stop();
73762
74458
  }
73763
- var finish = (code) => {
73764
- process.exitCode = code;
73765
- process.stdout.end(() => process.exit(code));
73766
- };
73767
74459
  main().then(() => finish(process.exitCode ?? 0)).catch((e) => {
73768
74460
  process.stderr.write(`Error: ${e.message}
73769
74461
  `);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@todoforai/cli",
3
- "version": "0.1.54",
3
+ "version": "0.1.55",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "todoforai-cli": "bin/todoforai-cli.js",