@todoforai/cli 0.1.53 → 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
  }
@@ -42875,6 +42875,9 @@ class ApiClient {
42875
42875
  setDefaultProject(projectId) {
42876
42876
  return this.request("POST", "/api/v1/users/default-project", { projectId });
42877
42877
  }
42878
+ updateProjectSettings(projectId, settings) {
42879
+ return this.request("PUT", `/api/v1/settings/projects/${projectId}`, { projectId, settings });
42880
+ }
42878
42881
  setProjectDefaultAgent(projectId, agentSettingsId) {
42879
42882
  return this.request("POST", "/api/v1/users/project-agent-mapping", { projectId, agentSettingsId });
42880
42883
  }
@@ -42923,6 +42926,19 @@ class ApiClient {
42923
42926
  upsertProjectGroup(projectId, slug, fields) {
42924
42927
  return this.request("PUT", `/api/v1/projects/${projectId}/groups/${encodeURIComponent(slug)}`, { projectId, slug, ...fields });
42925
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
+ }
42926
42942
  listProjectSurfaces(projectId) {
42927
42943
  return this.request("GET", `/api/v1/projects/${projectId}/surfaces`);
42928
42944
  }
@@ -43153,11 +43169,11 @@ class ApiClient {
43153
43169
  return res.json();
43154
43170
  }
43155
43171
  }
43156
- // ../packages/shared-api/src/frontend-ws.ts
43172
+ // ../../../home/hm/repo/todoforai/packages/shared-api/src/frontend-ws.ts
43157
43173
  import { randomUUID } from "crypto";
43158
43174
  import WebSocket from "ws";
43159
43175
 
43160
- // ../packages/shared-credentials/src/index.ts
43176
+ // ../../../home/hm/repo/todoforai/packages/shared-credentials/src/index.ts
43161
43177
  import { readFileSync, writeFileSync, mkdirSync, chmodSync } from "node:fs";
43162
43178
  import { join, dirname } from "node:path";
43163
43179
  import { homedir, platform } from "node:os";
@@ -43197,13 +43213,14 @@ function writeCredential(apiUrl, apiKey) {
43197
43213
  writeSecure(c);
43198
43214
  }
43199
43215
 
43200
- // ../packages/shared-api/src/frontend-ws.ts
43216
+ // ../../../home/hm/repo/todoforai/packages/shared-api/src/frontend-ws.ts
43201
43217
  var log = (level, ...args) => console.log(`[frontend-ws:${level}]`, ...args);
43202
43218
 
43203
43219
  class FrontendWebSocket {
43204
43220
  apiUrl;
43205
43221
  apiKey;
43206
43222
  ws = null;
43223
+ connecting = null;
43207
43224
  tabId = randomUUID();
43208
43225
  connected = false;
43209
43226
  callbacks = new Map;
@@ -43227,28 +43244,61 @@ class FrontendWebSocket {
43227
43244
  wsUrl = `wss://${url}`;
43228
43245
  return `${wsUrl}/ws/v1/frontend?tabId=${this.tabId}`;
43229
43246
  }
43230
- async connect() {
43247
+ connect() {
43231
43248
  if (this.connected && this.ws)
43232
- 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() {
43233
43256
  return new Promise((resolve) => {
43234
43257
  const wsUrl = this.getWsUrl();
43235
- this.ws = new WebSocket(wsUrl, [this.apiKey], { maxPayload: 5 * 1024 * 1024 });
43236
- 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;
43237
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?.();
43238
43281
  resolve(true);
43239
43282
  });
43240
- this.ws.on("message", (data) => {
43283
+ sock.on("message", (data) => {
43284
+ if (!current())
43285
+ return;
43241
43286
  try {
43242
43287
  this.handleMessage(JSON.parse(data.toString()));
43243
43288
  } catch {}
43244
43289
  });
43245
- this.ws.on("close", () => {
43290
+ sock.on("close", () => {
43291
+ clearInterval(heartbeat);
43292
+ if (!current())
43293
+ return;
43246
43294
  this.connected = false;
43247
43295
  this.settleAll();
43296
+ resolve(false);
43248
43297
  });
43249
- this.ws.on("error", (err) => {
43298
+ sock.on("error", (err) => {
43250
43299
  log("error", "WS error:", err.message);
43251
- this.connected = false;
43300
+ if (current())
43301
+ this.connected = false;
43252
43302
  resolve(false);
43253
43303
  });
43254
43304
  });
@@ -43317,7 +43367,7 @@ class FrontendWebSocket {
43317
43367
  }
43318
43368
  }
43319
43369
  }
43320
- async subscribe(todoId, callback) {
43370
+ async subscribe(todoId, callback, opts = {}) {
43321
43371
  if (!this.connected && !await this.connect())
43322
43372
  return false;
43323
43373
  if (callback)
@@ -43325,8 +43375,16 @@ class FrontendWebSocket {
43325
43375
  const res = await fetch(`${this.apiUrl}/api/v1/todos/${todoId}/subscribe`, {
43326
43376
  method: "POST",
43327
43377
  headers: { "x-api-key": this.apiKey, "x-tab-id": this.tabId, "Content-Type": "application/json" },
43328
- body: JSON.stringify({ todoId })
43378
+ body: JSON.stringify({ todoId }),
43379
+ signal: AbortSignal.timeout(15000)
43329
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
+ }
43330
43388
  return res.ok;
43331
43389
  }
43332
43390
  setCallback(todoId, callback) {
@@ -43336,17 +43394,16 @@ class FrontendWebSocket {
43336
43394
  this.callbacks.delete(todoId);
43337
43395
  }
43338
43396
  async sendInterrupt(projectId, todoId) {
43339
- if (!this.connected || !this.ws)
43397
+ const ws = this.ws;
43398
+ if (!this.connected || !ws)
43340
43399
  return false;
43341
- try {
43342
- this.ws.send(JSON.stringify({
43343
- type: "todo:interrupt_signal",
43344
- payload: { projectId, todoId }
43345
- }));
43346
- return true;
43347
- } catch {
43348
- return false;
43349
- }
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
+ });
43350
43407
  }
43351
43408
  async sendQueueCancel(todoId, messageId) {
43352
43409
  if (!this.connected || !this.ws)
@@ -43404,6 +43461,10 @@ class FrontendWebSocket {
43404
43461
  return this.completion(todoId);
43405
43462
  }
43406
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
+ }
43407
43468
  const early = this.completionResults.get(todoId);
43408
43469
  if (early) {
43409
43470
  this.completionResults.delete(todoId);
@@ -43422,7 +43483,7 @@ class FrontendWebSocket {
43422
43483
  });
43423
43484
  }
43424
43485
  }
43425
- // ../packages/shared-fbe/src/enums.ts
43486
+ // ../../../home/hm/repo/todoforai/packages/shared-fbe/src/enums.ts
43426
43487
  var TodoStatus;
43427
43488
  ((TodoStatus2) => {
43428
43489
  TodoStatus2["PROPOSED"] = "PROPOSED";
@@ -43446,7 +43507,7 @@ var TodoStatus;
43446
43507
  TodoStatus2["ARCHIVED"] = "ARCHIVED";
43447
43508
  TodoStatus2["DELETED"] = "DELETED";
43448
43509
  })(TodoStatus ||= {});
43449
- // ../packages/shared-fbe/src/models.ts
43510
+ // ../../../home/hm/repo/todoforai/packages/shared-fbe/src/models.ts
43450
43511
  var DEFAULT_MODEL = "anthropic:anthropic/claude-opus-5.5";
43451
43512
  var MODEL_ALIASES = {
43452
43513
  claude: DEFAULT_MODEL
@@ -43517,7 +43578,7 @@ function qualifiedModelIds(listing, filter) {
43517
43578
  }
43518
43579
  return q ? ids.filter((id) => id.toLowerCase().includes(q)) : ids;
43519
43580
  }
43520
- // ../packages/shared-fbe/src/billing.ts
43581
+ // ../../../home/hm/repo/todoforai/packages/shared-fbe/src/billing.ts
43521
43582
  var USAGE_WINDOWS = {
43522
43583
  SESSION_MS: 5 * 60 * 60 * 1000,
43523
43584
  WEEK_MS: 7 * 24 * 60 * 60 * 1000,
@@ -43525,7 +43586,7 @@ var USAGE_WINDOWS = {
43525
43586
  SESSION_FACTOR: 1 / 5
43526
43587
  };
43527
43588
  var NEVER_SCHEDULED_TIMESTAMP = 8640000000000000;
43528
- // ../packages/shared-fbe/src/mimetypes.json
43589
+ // ../../../home/hm/repo/todoforai/packages/shared-fbe/src/mimetypes.json
43529
43590
  var mimetypes_default = {
43530
43591
  conversionProfiles: {
43531
43592
  to_jpeg_90: { toMime: "image/jpeg", toExt: "jpg", quality: 0.9 }
@@ -43618,7 +43679,7 @@ var mimetypes_default = {
43618
43679
  }
43619
43680
  };
43620
43681
 
43621
- // ../packages/shared-fbe/src/attachmentUtils.ts
43682
+ // ../../../home/hm/repo/todoforai/packages/shared-fbe/src/attachmentUtils.ts
43622
43683
  var MCP_TEXT_MIME = Object.entries(mimetypes_default.aliases).find(([, type]) => type === "mcp_text")[0];
43623
43684
  var MCP_AUDIO_MIME = Object.entries(mimetypes_default.aliases).find(([, type]) => type === "mcp_audio")[0];
43624
43685
  var AttachmentType = Object.fromEntries(Object.keys(mimetypes_default.types).map((k) => [k.toUpperCase(), k]));
@@ -43658,7 +43719,7 @@ for (const [ext, entry] of Object.entries(mimetypes_default.extensions)) {
43658
43719
  if (!MIME_TO_EXT[mime])
43659
43720
  MIME_TO_EXT[mime] = ext;
43660
43721
  }
43661
- // ../packages/shared-fbe/src/edgeTypes.ts
43722
+ // ../../../home/hm/repo/todoforai/packages/shared-fbe/src/edgeTypes.ts
43662
43723
  var NON_SHELL_TYPES = new Set([
43663
43724
  "WEB_EXTENSION" /* WEB_EXTENSION */,
43664
43725
  "EXTENSION" /* EXTENSION */,
@@ -43666,7 +43727,7 @@ var NON_SHELL_TYPES = new Set([
43666
43727
  "ANDROID" /* ANDROID */,
43667
43728
  "IOS" /* IOS */
43668
43729
  ]);
43669
- // ../packages/shared-fbe/src/blocks/index.ts
43730
+ // ../../../home/hm/repo/todoforai/packages/shared-fbe/src/blocks/index.ts
43670
43731
  var USER_INPUT_BLOCK_TYPES = new Set([
43671
43732
  "question" /* Question */,
43672
43733
  "login_prompt" /* Login */,
@@ -43720,7 +43781,7 @@ var FILE_TOOL_TYPES = new Set([
43720
43781
  "edit" /* Edit */,
43721
43782
  "modify_file" /* ModifyFile */
43722
43783
  ]);
43723
- // ../packages/shared-fbe/src/channels.ts
43784
+ // ../../../home/hm/repo/todoforai/packages/shared-fbe/src/channels.ts
43724
43785
  var privateSuffix = (privateOwnerId) => privateOwnerId ? `:private:${privateOwnerId}` : "";
43725
43786
  var SERVER_TO_FRONTENDS = {
43726
43787
  task: {
@@ -43779,6 +43840,7 @@ var SERVER_TO_FRONTENDS = {
43779
43840
  msgMetaAi: (todoId) => `${"todo:msg_meta_ai" /* TODO_MSG_META_AI */}:${todoId}`,
43780
43841
  msgQueueUpdate: (todoId) => `${"todo:msg_queue_update" /* TODO_MSG_QUEUE_UPDATE */}:${todoId}`,
43781
43842
  message_removed: (todoId) => `${"todo:message_removed" /* TODO_MESSAGE_REMOVED */}:${todoId}`,
43843
+ commentUpdate: (todoId) => `${"todo:comment_update" /* TODO_COMMENT_UPDATE */}:${todoId}`,
43782
43844
  start_universal: (todoId) => `${"block:start_universal" /* BLOCK_START_UNIVERSAL */}:${todoId}`,
43783
43845
  message: (todoId) => `${"block:message" /* BLOCK_MESSAGE */}:${todoId}`,
43784
43846
  end: (todoId) => `${"block:end" /* BLOCK_END */}:${todoId}`,
@@ -43809,7 +43871,7 @@ var SERVER_TO_FRONTENDS = {
43809
43871
  function_call_request: (userId) => `${"FUNCTION_CALL_REQUEST_AGENT" /* FUNCTION_CALL_REQUEST_AGENT */}:frontend:${userId}`
43810
43872
  }
43811
43873
  };
43812
- // ../packages/shared-fbe/src/REST_types.ts
43874
+ // ../../../home/hm/repo/todoforai/packages/shared-fbe/src/REST_types.ts
43813
43875
  var MANUAL_CHANNEL = "Manual";
43814
43876
  function manualAnswersOf(sources) {
43815
43877
  const manual = sources?.find((s) => s.channel === MANUAL_CHANNEL);
@@ -43820,7 +43882,8 @@ function manualAnswersOf(sources) {
43820
43882
  return out;
43821
43883
  }
43822
43884
  var manualSamples = (answers) => Object.entries(answers).filter(([, a]) => a.trim()).map(([prompt, text]) => ({ prompt, text: text.trim() }));
43823
- // ../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
43824
43887
  var USER_HEADINGS = new Set([
43825
43888
  "your task",
43826
43889
  "your tasks",
@@ -43829,7 +43892,7 @@ var USER_HEADINGS = new Set([
43829
43892
  "steps",
43830
43893
  "data needed"
43831
43894
  ]);
43832
- // ../packages/shared-fbe/src/bashPatterns.ts
43895
+ // ../../../home/hm/repo/todoforai/packages/shared-fbe/src/bashPatterns.ts
43833
43896
  function splitShellCommands(input) {
43834
43897
  const parts = [];
43835
43898
  let current = "";
@@ -44071,7 +44134,7 @@ function getBlockServerId(block) {
44071
44134
  return prefix ? prefix.slice(0, -1) : "*";
44072
44135
  }
44073
44136
 
44074
- // ../packages/shared-fbe/src/permissionUtils.ts
44137
+ // ../../../home/hm/repo/todoforai/packages/shared-fbe/src/permissionUtils.ts
44075
44138
  function parsePattern(pattern) {
44076
44139
  const colonIndex = pattern.indexOf(":");
44077
44140
  if (colonIndex === -1)
@@ -44172,7 +44235,7 @@ function getBlockNewPatterns(block, permissions) {
44172
44235
  }
44173
44236
  return pairs.filter(({ raw, pattern }) => !isPatternAllowed(permissions, pattern) && !allow.some((rule) => bashRuleMatchesCmd(rule, serverId, raw))).map(({ pattern }) => pattern);
44174
44237
  }
44175
- // ../packages/shared-fbe/src/systemAgents.ts
44238
+ // ../../../home/hm/repo/todoforai/packages/shared-fbe/src/systemAgents.ts
44176
44239
  var BUSINESS_ANALYZER_AGENT = {
44177
44240
  id: "00000000-0000-0000-0000-202601013151",
44178
44241
  name: "Business Analyzer",
@@ -44318,7 +44381,7 @@ var SYSTEM_AGENTS = {
44318
44381
  [ONBOARDING_AGENT.id]: ONBOARDING_AGENT
44319
44382
  };
44320
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.";
44321
- // ../packages/shared-fbe/src/outputLimits.ts
44384
+ // ../../../home/hm/repo/todoforai/packages/shared-fbe/src/outputLimits.ts
44322
44385
  var MAX_LINE_LEN = 300;
44323
44386
  var STREAM_FIRST = 1e4;
44324
44387
  var STREAM_LAST = 1e4;
@@ -44329,7 +44392,7 @@ var OUTPUT_POLICIES = {
44329
44392
  full: { firstLimit: Infinity, lastLimit: 0, hardCap: RUN_OUTPUT_CAP, lineLimit: MAX_LINE_LEN },
44330
44393
  raw: { firstLimit: Infinity, lastLimit: 0, hardCap: Infinity, lineLimit: Infinity }
44331
44394
  };
44332
- // ../packages/shared-fbe/src/model_promos.json
44395
+ // ../../../home/hm/repo/todoforai/packages/shared-fbe/src/model_promos.json
44333
44396
  var model_promos_default = {
44334
44397
  _doc: [
44335
44398
  "Single source of truth for TODOforAI promotional model discounts.",
@@ -44371,10 +44434,10 @@ var model_promos_default = {
44371
44434
  ]
44372
44435
  };
44373
44436
 
44374
- // ../packages/shared-fbe/src/modelPromos.ts
44437
+ // ../../../home/hm/repo/todoforai/packages/shared-fbe/src/modelPromos.ts
44375
44438
  var BOOST_FACTOR = model_promos_default.boost_remaining_price_factor;
44376
44439
  var MODEL_PROMOS = model_promos_default.promos;
44377
- // ../packages/shared-fbe/src/realtime/topics.ts
44440
+ // ../../../home/hm/repo/todoforai/packages/shared-fbe/src/realtime/topics.ts
44378
44441
  var TOPICS = {
44379
44442
  ["todo:new" /* NEW_TODO */]: {
44380
44443
  channel: (p) => SERVER_TO_FRONTENDS.project.new_todo(p.projectId, p.privateOwnerId),
@@ -44480,6 +44543,10 @@ var TOPICS = {
44480
44543
  channel: (p) => SERVER_TO_FRONTENDS.todo.message_removed(p.todoId),
44481
44544
  audience: "frontend"
44482
44545
  },
44546
+ ["todo:comment_update" /* TODO_COMMENT_UPDATE */]: {
44547
+ channel: (p) => SERVER_TO_FRONTENDS.todo.commentUpdate(p.todoId),
44548
+ audience: "frontend"
44549
+ },
44483
44550
  ["todo:msg_meta_usr" /* TODO_MSG_META_USR */]: {
44484
44551
  channel: (p) => SERVER_TO_FRONTENDS.todo.msgMetaUsr(p.todoId),
44485
44552
  audience: "frontend"
@@ -44537,12 +44604,12 @@ var TOPICS = {
44537
44604
  audience: "frontend"
44538
44605
  }
44539
44606
  };
44540
- // ../packages/shared-fbe/src/modelNames.ts
44607
+ // ../../../home/hm/repo/todoforai/packages/shared-fbe/src/modelNames.ts
44541
44608
  var LEGACY_MODEL_MAPPING = {
44542
44609
  claude: DEFAULT_MODEL.slice(DEFAULT_MODEL.indexOf(":") + 1),
44543
44610
  "gemini-pro": "google/gemini-3.1-pro-preview"
44544
44611
  };
44545
- // ../packages/shared-credentials/src/index.ts
44612
+ // ../../../home/hm/repo/todoforai/packages/shared-credentials/src/index.ts
44546
44613
  function normalizeApiUrl2(url) {
44547
44614
  if (url.startsWith("localhost"))
44548
44615
  return `http://${url}`;
@@ -44556,7 +44623,7 @@ import { parseArgs } from "util";
44556
44623
  // package.json
44557
44624
  var package_default = {
44558
44625
  name: "@todoforai/cli",
44559
- version: "0.1.53",
44626
+ version: "0.1.55",
44560
44627
  type: "module",
44561
44628
  bin: {
44562
44629
  "todoforai-cli": "bin/todoforai-cli.js",
@@ -44624,6 +44691,9 @@ Examples:
44624
44691
  tfa-cli show list|rm … # List / take down shown files
44625
44692
  tfa-cli open <url> [todo-id] # Live url preview in the chat
44626
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
44627
44697
  tfa-cli claim mint --seed <projectId> [--emails a@x,b@y] [--ttl <sec>] # Mint /claim/<token> links for a project you own
44628
44698
  tfa-cli next [--direction "<text>"] # Analyzer growth recommendation cards
44629
44699
  tfa-cli <command> help # Details per command
@@ -44704,7 +44774,7 @@ var WORD_FLAGS = {
44704
44774
  agents: "list-agents",
44705
44775
  config: "show-config"
44706
44776
  };
44707
- 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"];
44708
44778
  function parseCliArgs() {
44709
44779
  const { values, positionals } = parseArgs({
44710
44780
  args: process.argv.slice(2),
@@ -44739,6 +44809,10 @@ function parseCliArgs() {
44739
44809
  seed: { type: "string" },
44740
44810
  emails: { type: "string" },
44741
44811
  ttl: { type: "string" },
44812
+ subject: { type: "string" },
44813
+ href: { type: "string" },
44814
+ unread: { type: "boolean", default: false },
44815
+ limit: { type: "string" },
44742
44816
  resume: { type: "string", short: "r" },
44743
44817
  continue: { type: "boolean", short: "c", default: false },
44744
44818
  "non-interactive": { type: "boolean", short: "n", default: false },
@@ -46996,7 +47070,88 @@ function renderDiff(originalContent, modifiedContent, filePath) {
46996
47070
  return parts.join("");
46997
47071
  }
46998
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
+
46999
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
+ }
47000
47155
  var diffStoreByWs = new WeakMap;
47001
47156
  function classifyBlock(info) {
47002
47157
  const inner = (info.block_type || "").toLowerCase();
@@ -47078,12 +47233,8 @@ async function watchTodo(ws, todoId, projectId, opts = {}) {
47078
47233
  process.removeAllListeners("SIGINT");
47079
47234
  process.on("SIGINT", () => {
47080
47235
  interruptCount++;
47081
- if (interruptCount >= 2) {
47082
- process.stderr.write(`
47083
- ${RED}Force exit (double Ctrl+C)${RESET}
47084
- `);
47085
- process.exit(130);
47086
- }
47236
+ if (opts.exitOnInterrupt || interruptCount >= 2)
47237
+ return void shutdown(130, "Cancelled by user (Ctrl+C)");
47087
47238
  process.stderr.write(`
47088
47239
  ${YELLOW}Interrupting... (Ctrl+C again to force exit)${RESET}
47089
47240
  `);
@@ -47282,8 +47433,22 @@ ${DIM}[todo:status] ${status}${RESET}
47282
47433
  callback(msgType, payload);
47283
47434
  }
47284
47435
  }
47436
+ setActiveRun({ ws, projectId, todoId });
47285
47437
  try {
47286
- 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);
47287
47452
  process.stdout.write(`
47288
47453
  `);
47289
47454
  if (!result?.success) {
@@ -47296,6 +47461,8 @@ ${DIM}[todo:status] ${status}${RESET}
47296
47461
  }
47297
47462
  return true;
47298
47463
  } catch (e) {
47464
+ await cancelActiveRun();
47465
+ process.exitCode = 1;
47299
47466
  if (!opts.suppressCancelNotice) {
47300
47467
  process.stderr.write(`${YELLOW}Interrupted${RESET}
47301
47468
  `);
@@ -47860,7 +48027,7 @@ function printProjectHelp() {
47860
48027
  process.stderr.write(`
47861
48028
  tfa-cli project — edit the current project
47862
48029
 
47863
- --project <id> or $TODOFORAI_PROJECT_ID selects the project.
48030
+ --project <id>, $TODOFORAI_PROJECT_ID, or the configured default selects the project.
47864
48031
 
47865
48032
  Usage:
47866
48033
  tfa-cli project list Projects you can access (* = default)
@@ -47893,6 +48060,7 @@ var TODO_ALIASES = {
47893
48060
  agent: "agentSettingsId",
47894
48061
  public: "isPublic"
47895
48062
  };
48063
+ var NO_PROJECT = "No project — pass --project <id> or set TODOFORAI_PROJECT_ID";
47896
48064
  function fail(msg) {
47897
48065
  process.stderr.write(`${RED}${msg}${RESET}
47898
48066
  `);
@@ -47943,9 +48111,8 @@ async function todoCommand(api, positionals, args) {
47943
48111
  process.stderr.write(`${GREEN}✅ ${todoId} updated: ${Object.keys(updates).join(", ")}${RESET}
47944
48112
  `);
47945
48113
  }
47946
- async function projectCommand(api, positionals, args) {
48114
+ async function projectCommand(api, positionals, args, projectId) {
47947
48115
  const [, sub, ...rest] = positionals;
47948
- const projectId = args.project || getEnv2("PROJECT_ID");
47949
48116
  if (!sub) {
47950
48117
  printProjectHelp();
47951
48118
  process.exit(0);
@@ -47964,7 +48131,7 @@ async function projectCommand(api, positionals, args) {
47964
48131
  return;
47965
48132
  }
47966
48133
  if (!projectId)
47967
- fail("No project — pass --project <id> or set TODOFORAI_PROJECT_ID");
48134
+ fail(NO_PROJECT);
47968
48135
  if (sub === "set") {
47969
48136
  if (!rest.length)
47970
48137
  fail("Usage: tfa-cli project set <field=value>…");
@@ -48077,7 +48244,7 @@ function printBrandHelp() {
48077
48244
  process.stderr.write(`
48078
48245
  tfa-cli brand — the project's brand page and its learned writing voice
48079
48246
 
48080
- The project IS the brand. --project <id> or $TODOFORAI_PROJECT_ID selects it.
48247
+ The project IS the brand. --project <id>, $TODOFORAI_PROJECT_ID, or the configured default selects it.
48081
48248
  The brand page text itself is a file: read/write todoforai:business-context.
48082
48249
 
48083
48250
  Usage:
@@ -48107,15 +48274,9 @@ Questions the UI asks (keys for 'answers'):
48107
48274
  "A frustrated customer says things aren't working. How do you reply?"
48108
48275
  `);
48109
48276
  }
48110
- function requireProject(args) {
48111
- const projectId = args.project || getEnv2("PROJECT_ID");
48112
- if (!projectId)
48113
- fail("No project — pass --project <id> or set TODOFORAI_PROJECT_ID");
48114
- return projectId;
48115
- }
48116
- async function brandCommand(api, positionals, args) {
48277
+ async function brandCommand(api, positionals, args, scoped) {
48117
48278
  const [, sub, ...rest] = positionals;
48118
- const projectId = requireProject(args);
48279
+ const projectId = scoped || fail(NO_PROJECT);
48119
48280
  if (!sub || sub === "show") {
48120
48281
  const brand = await api.getBusinessContext(projectId);
48121
48282
  if (args.json) {
@@ -48572,6 +48733,425 @@ async function listTodosCommand(api, defaultProjectId, argv) {
48572
48733
  `);
48573
48734
  }
48574
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
+
48575
49155
  // src/index.ts
48576
49156
  import { randomUUID as randomUUID3 } from "crypto";
48577
49157
 
@@ -48836,7 +49416,7 @@ async function spawnMayflyBridge(apiUrl, todoId, workspace, opts = {}) {
48836
49416
  }
48837
49417
  });
48838
49418
  });
48839
- return { child, stop };
49419
+ return { child, stop, stopped: () => stopped };
48840
49420
  }
48841
49421
 
48842
49422
  // src/acp.ts
@@ -72901,6 +73481,93 @@ async function runAcp(apiUrl, apiKey, opts) {
72901
73481
  await agent2.close();
72902
73482
  }
72903
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
+
72904
73571
  // src/index.ts
72905
73572
  try {
72906
73573
  const pkgPath = path4.resolve(fileURLToPath(import.meta.url), "../../package.json");
@@ -72976,7 +73643,7 @@ async function interactiveLoop(ws, api2, todoId, projectId, agent2, json2, autoA
72976
73643
  cfg.addToHistory(input2);
72977
73644
  process.stderr.write("\u2500".repeat(40) + `
72978
73645
  `);
72979
- await api2.addMessage(projectId, input2, agent2, todoId);
73646
+ await appendTracked(api2, ws, projectId, input2, agent2, todoId);
72980
73647
  await watchTodo(ws, todoId, projectId, {
72981
73648
  json: json2,
72982
73649
  autoApprove,
@@ -72987,6 +73654,11 @@ async function interactiveLoop(ws, api2, todoId, projectId, agent2, json2, autoA
72987
73654
  }
72988
73655
  }
72989
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
+ }
72990
73662
  async function linkToSpawningBlock(api2, todoId) {
72991
73663
  const { TODOFORAI_TODO_ID: pTodo, TODOFORAI_MESSAGE_ID: pMsg, TODOFORAI_BLOCK_ID: pBlock } = process.env;
72992
73664
  if (!pTodo || !pMsg || !pBlock || pTodo === todoId)
@@ -72995,12 +73667,7 @@ async function linkToSpawningBlock(api2, todoId) {
72995
73667
  `));
72996
73668
  }
72997
73669
  async function main() {
72998
- process.on("SIGINT", () => {
72999
- process.stderr.write(`
73000
- Cancelled by user (Ctrl+C)
73001
- `);
73002
- process.exit(130);
73003
- });
73670
+ installSignalHandlers();
73004
73671
  const { values: args, positionals } = parseCliArgs();
73005
73672
  const groupTag = args.group === undefined ? getEnv("GROUP_ID") : String(args.group);
73006
73673
  const groupName = args["group-name"] === undefined ? undefined : String(args["group-name"]);
@@ -73045,11 +73712,19 @@ Cancelled by user (Ctrl+C)
73045
73712
  printDeviceHelp();
73046
73713
  process.exit(0);
73047
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
+ }
73048
73723
  if ((positionals[0] === "list" || positionals[0] === "ls") && args.help) {
73049
73724
  printListTodosHelp();
73050
73725
  process.exit(0);
73051
73726
  }
73052
- 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])) {
73053
73728
  printUsage();
73054
73729
  process.exit(0);
73055
73730
  }
@@ -73167,18 +73842,27 @@ Cancelled by user (Ctrl+C)
73167
73842
  await todoCommand(api2, positionals, args);
73168
73843
  return;
73169
73844
  }
73845
+ const scopedProject = args.project || getEnv("PROJECT_ID") || cfgScope.data.default_project_id || undefined;
73170
73846
  if (positionals[0] === "project") {
73171
- await projectCommand(api2, positionals, args);
73847
+ await projectCommand(api2, positionals, args, scopedProject);
73172
73848
  return;
73173
73849
  }
73174
73850
  if (positionals[0] === "brand") {
73175
- await brandCommand(api2, positionals, args);
73851
+ await brandCommand(api2, positionals, args, scopedProject);
73176
73852
  return;
73177
73853
  }
73178
73854
  if (positionals[0] === "device") {
73179
73855
  await deviceCommand(api2, positionals, args);
73180
73856
  return;
73181
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
+ }
73182
73866
  if (positionals[0] === "delete") {
73183
73867
  const todoId = positionals[1];
73184
73868
  if (!todoId) {
@@ -73535,6 +74219,7 @@ Cancelled by user (Ctrl+C)
73535
74219
  }
73536
74220
  if (!args["no-watch"]) {
73537
74221
  const ws2 = new FrontendWebSocket(apiUrl, apiKey);
74222
+ setActiveRun2({ ws: ws2, projectId: projectId2, todoId });
73538
74223
  await ws2.connect();
73539
74224
  const autoApprove = !!args["dangerously-skip-permissions"];
73540
74225
  let agent3 = todo2.agentSettings || { id: todo2.agentSettingsId };
@@ -73545,7 +74230,8 @@ Cancelled by user (Ctrl+C)
73545
74230
  await watchTodo(ws2, todoId, projectId2, {
73546
74231
  json: !!args.json,
73547
74232
  autoApprove,
73548
- agentSettings: agent3
74233
+ agentSettings: agent3,
74234
+ exitOnInterrupt: !!args["non-interactive"]
73549
74235
  });
73550
74236
  if (!args["non-interactive"]) {
73551
74237
  process.stderr.write(`
@@ -73594,9 +74280,9 @@ Resumed: ${CYAN2}${getFrontendUrl(apiUrl, todoId)}${RESET2}
73594
74280
  const followUp = promptArg(positionals) ?? (process.stdin.isTTY ? "" : await readStdin());
73595
74281
  if (followUp) {
73596
74282
  cfg.addToHistory(followUp);
73597
- await api2.addMessage(projectId2, followUp, agent3, todoId);
74283
+ await appendTracked(api2, ws2, projectId2, followUp, agent3, todoId);
73598
74284
  await linkToSpawningBlock(api2, todoId);
73599
- 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"] });
73600
74286
  }
73601
74287
  if (!args["non-interactive"]) {
73602
74288
  await interactiveLoop(ws2, api2, todoId, projectId2, agent3, !!args.json, autoApprove, cfg);
@@ -73615,9 +74301,15 @@ Resumed: ${CYAN2}${getFrontendUrl(apiUrl, todoId)}${RESET2}
73615
74301
  const bridgeReady = !args["no-bridge"] && !args["no-watch"] && !args.isolated ? ensureBridgeRunning2(apiUrl, apiKey) : null;
73616
74302
  const isolatedTodoId = args.isolated ? randomUUID3() : undefined;
73617
74303
  const mayfly = isolatedTodoId ? await spawnMayflyBridge(apiUrl, isolatedTodoId, realpathSync2(resolve4(args.path || ".")), { debug: !!args.debug, apiKey }) : null;
73618
- if (mayfly)
74304
+ if (mayfly) {
73619
74305
  process.stderr.write(`${DIM2}Isolated bridge:${RESET2} ${CYAN2}mayfly-${isolatedTodoId}${RESET2}
73620
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
+ }
73621
74313
  let preMatchedAgent = null;
73622
74314
  let agents = null;
73623
74315
  if (args.agent) {
@@ -73721,15 +74413,16 @@ Resumed: ${CYAN2}${getFrontendUrl(apiUrl, todoId)}${RESET2}
73721
74413
  agent2 = { ...agent2, permissions: { ...perms, allow: [...perms.allow || [], "*:*"] } };
73722
74414
  }
73723
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));
73724
74419
  let todo;
73725
74420
  try {
73726
- todo = await api2.addMessage(projectId, content, agent2, isolatedTodoId, undefined, undefined, groupTag || undefined, groupName);
74421
+ todo = await created;
73727
74422
  } catch (e) {
73728
74423
  if (!args.project && !envProjectId && cfgScope.data.default_project_id === projectId && /failed: 403/.test(e.message || "")) {
73729
74424
  cfgScope.clearDefaultProject();
73730
- process.stderr.write(`${RED3}Not authorized for cached default project ${projectName} (${projectId}) \u2014 cleared it. Re-run to pick a project.${RESET2}
73731
- `);
73732
- process.exit(1);
74425
+ throw new Error(`Not authorized for cached default project ${projectName} (${projectId}) \u2014 cleared it. Re-run to pick a project.`);
73733
74426
  }
73734
74427
  throw e;
73735
74428
  }
@@ -73750,7 +74443,8 @@ Resumed: ${CYAN2}${getFrontendUrl(apiUrl, todoId)}${RESET2}
73750
74443
  await watchTodo(ws, actualTodoId, projectId, {
73751
74444
  json: !!args.json,
73752
74445
  autoApprove,
73753
- agentSettings: agent2
74446
+ agentSettings: agent2,
74447
+ exitOnInterrupt: !!args["non-interactive"]
73754
74448
  });
73755
74449
  if (!args["non-interactive"]) {
73756
74450
  process.stderr.write(`
@@ -73762,10 +74456,6 @@ ${"\u2500".repeat(40)}
73762
74456
  }
73763
74457
  mayfly?.stop();
73764
74458
  }
73765
- var finish = (code) => {
73766
- process.exitCode = code;
73767
- process.stdout.end(() => process.exit(code));
73768
- };
73769
74459
  main().then(() => finish(process.exitCode ?? 0)).catch((e) => {
73770
74460
  process.stderr.write(`Error: ${e.message}
73771
74461
  `);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@todoforai/cli",
3
- "version": "0.1.53",
3
+ "version": "0.1.55",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "todoforai-cli": "bin/todoforai-cli.js",