@todoforai/cli 0.1.54 → 0.1.56
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 +11 -0
- package/bin/todoforai-cli.js +5 -3
- package/dist/todoforai-cli.js +2411 -226
- package/package.json +1 -1
package/dist/todoforai-cli.js
CHANGED
|
@@ -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
|
}
|
|
@@ -43207,6 +43220,7 @@ 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
|
-
|
|
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
|
-
|
|
43239
|
-
this.ws
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
43298
|
+
sock.on("error", (err) => {
|
|
43253
43299
|
log("error", "WS error:", err.message);
|
|
43254
|
-
|
|
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
|
-
|
|
43343
|
-
|
|
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);
|
|
@@ -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}`,
|
|
@@ -43823,6 +43882,7 @@ 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() }));
|
|
43885
|
+
var senderLabel = (s) => s.kind === "team" ? `${s.name} · TODO for AI team` : `${s.name} · your agent`;
|
|
43826
43886
|
// ../packages/shared-fbe/src/templateBody.ts
|
|
43827
43887
|
var USER_HEADINGS = new Set([
|
|
43828
43888
|
"your task",
|
|
@@ -44332,6 +44392,1499 @@ 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
|
};
|
|
44395
|
+
// ../packages/shared-fbe/src/tool_catalog.json
|
|
44396
|
+
var tool_catalog_default = {
|
|
44397
|
+
zele: {
|
|
44398
|
+
category: "email",
|
|
44399
|
+
pkg: "zele",
|
|
44400
|
+
installer: "npm",
|
|
44401
|
+
label: "Gmail",
|
|
44402
|
+
summary: "Read and send Gmail from your own account.",
|
|
44403
|
+
statusCmd: "zele whoami 2>/dev/null | grep 'email:' | grep -m1 .",
|
|
44404
|
+
loginNeedsPty: true,
|
|
44405
|
+
loginCmd: "zele login --method google",
|
|
44406
|
+
oauthUrlPattern: "(https://accounts\\.google\\.com\\S+)",
|
|
44407
|
+
oauthCallbackPort: 8089,
|
|
44408
|
+
credentialPaths: ["~/.zele/sqlite.db"],
|
|
44409
|
+
versionCmd: "zele --version 2>/dev/null | sed -n 1p",
|
|
44410
|
+
preinstallCloud: true,
|
|
44411
|
+
preinstallJarvis: true,
|
|
44412
|
+
description: "Gmail (multi-account, OAuth login): inbox, search, send, reply, contacts. Google Calendar via `zele cal`: list/create/update/delete events (Meet links, recurrence), respond to invites, free/busy."
|
|
44413
|
+
},
|
|
44414
|
+
"zele-calendar": {
|
|
44415
|
+
preinstallCloud: true,
|
|
44416
|
+
category: "utility",
|
|
44417
|
+
pkg: "zele",
|
|
44418
|
+
installer: "npm",
|
|
44419
|
+
binName: "zele",
|
|
44420
|
+
label: "Google Calendar",
|
|
44421
|
+
summary: "See your Google Calendar and book, move or cancel events.",
|
|
44422
|
+
statusCmd: "zele whoami 2>/dev/null | grep 'email:' | grep -m1 .",
|
|
44423
|
+
loginNeedsPty: true,
|
|
44424
|
+
loginCmd: "zele login --method google",
|
|
44425
|
+
oauthUrlPattern: "(https://accounts\\.google\\.com\\S+)",
|
|
44426
|
+
oauthCallbackPort: 8089,
|
|
44427
|
+
credentialPaths: ["~/.zele/sqlite.db"],
|
|
44428
|
+
versionCmd: "zele --version 2>/dev/null | sed -n 1p",
|
|
44429
|
+
preinstallJarvis: true,
|
|
44430
|
+
description: "Google Calendar via `zele cal`: list calendars and events, create/update/delete events with Google Meet links, recurrence and reminders, respond to invites, free/busy."
|
|
44431
|
+
},
|
|
44432
|
+
xurl: {
|
|
44433
|
+
category: "social",
|
|
44434
|
+
pkg: "@xdevplatform/xurl",
|
|
44435
|
+
installer: "npm",
|
|
44436
|
+
label: "X",
|
|
44437
|
+
summary: "Post to X and read timelines, replies and DMs.",
|
|
44438
|
+
statusCmd: `xurl whoami 2>&1 | sed -n 's/.*"username"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/p' | grep -m10 .`,
|
|
44439
|
+
loginCmd: "xurl auth oauth2",
|
|
44440
|
+
loginTerminalHint: "X needs your own developer app first: create one at developer.x.com (callback `http://localhost:8080/callback`), run `xurl auth apps add tfa --client-id <id> --client-secret <secret>`, then `xurl auth oauth2`.",
|
|
44441
|
+
oauthUrlPattern: "(https://(?:twitter|x)\\.com/i/oauth2\\S+)",
|
|
44442
|
+
oauthCallbackPort: 8080,
|
|
44443
|
+
credentialPaths: ["~/.xurl"],
|
|
44444
|
+
description: "Raw X API access via `xurl <method> <path>`.",
|
|
44445
|
+
versionCmd: "xurl version 2>/dev/null | sed -n 1p",
|
|
44446
|
+
preinstallJarvis: true,
|
|
44447
|
+
preinstallCloud: true
|
|
44448
|
+
},
|
|
44449
|
+
"linkedin-api": {
|
|
44450
|
+
preinstallCloud: true,
|
|
44451
|
+
category: "social",
|
|
44452
|
+
pkg: "@todoforai/linkedin-api",
|
|
44453
|
+
installer: "npm",
|
|
44454
|
+
label: "LinkedIn",
|
|
44455
|
+
summary: "Read LinkedIn profiles and send messages and connection requests.",
|
|
44456
|
+
statusCmd: `linkedin-api whoami 2>/dev/null | sed -n 's/.*"publicId"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/p' | grep -m10 .`,
|
|
44457
|
+
loginCmd: "linkedin-api auth <li_at> <jsessionid>",
|
|
44458
|
+
credentialPaths: ["~/.config/linkedin-api/credentials.json"],
|
|
44459
|
+
description: "LinkedIn Voyager API CLI for profiles, conversations, DMs, and connections.",
|
|
44460
|
+
versionCmd: "linkedin-api --version 2>/dev/null | sed -n 1p"
|
|
44461
|
+
},
|
|
44462
|
+
"figma-api": {
|
|
44463
|
+
category: "design",
|
|
44464
|
+
pkg: "@todoforai/figma-api",
|
|
44465
|
+
installer: "npm",
|
|
44466
|
+
label: "Figma",
|
|
44467
|
+
summary: "Read Figma files, export images and leave comments.",
|
|
44468
|
+
statusCmd: `figma-api me 2>/dev/null | sed -n 's/.*"email"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/p' | grep -m10 .`,
|
|
44469
|
+
loginCmd: "figma-api auth <token>",
|
|
44470
|
+
credentialPaths: ["~/.config/figma-api/credentials.json"],
|
|
44471
|
+
description: "Figma REST API CLI. File commands take a file key or Figma URL. Canvas writes (frames/text/shapes) use `figma-api run` via the plugin bridge. Run `figma-api <cmd> --help` for params.",
|
|
44472
|
+
versionCmd: "figma-api --version 2>/dev/null | sed -n 1p"
|
|
44473
|
+
},
|
|
44474
|
+
"slack-cli": {
|
|
44475
|
+
preinstallCloud: true,
|
|
44476
|
+
cloudInstallCmd: "curl -fsSL https://github.com/slackapi/slack-cli/releases/download/v4.2.0/slack_cli_4.2.0_linux_amd64.tar.gz | tar -xz -C /tmp bin/slack && install -m0755 /tmp/bin/slack /usr/local/bin/slack-cli && rm -rf /tmp/bin",
|
|
44477
|
+
cloudVerifyCmd: "slack-cli --version",
|
|
44478
|
+
category: "social",
|
|
44479
|
+
pkg: "slack-cli",
|
|
44480
|
+
installer: "binary",
|
|
44481
|
+
label: "Slack",
|
|
44482
|
+
summary: "Work with Slack workspaces and apps.",
|
|
44483
|
+
statusCmd: "slack-cli auth list 2>&1 | grep 'Team' >/dev/null && echo authenticated",
|
|
44484
|
+
loginCmd: "slack-cli login",
|
|
44485
|
+
loginTerminalHint: "Run `slack-cli login` in a terminal on this host, paste the `/slackauthticket …` command it prints into any Slack channel, then type the challenge code Slack shows you back into the terminal.",
|
|
44486
|
+
credentialPaths: ["~/.slack/credentials.json"],
|
|
44487
|
+
description: "Invoked as `slack-cli` to avoid colliding with the Slack desktop app’s `slack` binary.",
|
|
44488
|
+
versionCmd: "slack-cli version 2>/dev/null | sed -n 1p",
|
|
44489
|
+
binName: "slack-cli",
|
|
44490
|
+
binary: {
|
|
44491
|
+
"linux-x86_64": {
|
|
44492
|
+
url: "https://github.com/slackapi/slack-cli/releases/download/v4.2.0/slack_cli_4.2.0_linux_amd64.tar.gz",
|
|
44493
|
+
archive: "tar.gz",
|
|
44494
|
+
extract: "bin/slack"
|
|
44495
|
+
},
|
|
44496
|
+
"linux-aarch64": {
|
|
44497
|
+
url: "https://github.com/slackapi/slack-cli/releases/download/v4.2.0/slack_cli_4.2.0_linux_arm64.tar.gz",
|
|
44498
|
+
archive: "tar.gz",
|
|
44499
|
+
extract: "bin/slack"
|
|
44500
|
+
},
|
|
44501
|
+
"darwin-x86_64": {
|
|
44502
|
+
url: "https://github.com/slackapi/slack-cli/releases/download/v4.2.0/slack_cli_4.2.0_macOS_amd64.tar.gz",
|
|
44503
|
+
archive: "tar.gz",
|
|
44504
|
+
extract: "bin/slack"
|
|
44505
|
+
},
|
|
44506
|
+
"darwin-aarch64": {
|
|
44507
|
+
url: "https://github.com/slackapi/slack-cli/releases/download/v4.2.0/slack_cli_4.2.0_macOS_arm64.tar.gz",
|
|
44508
|
+
archive: "tar.gz",
|
|
44509
|
+
extract: "bin/slack"
|
|
44510
|
+
},
|
|
44511
|
+
"windows-x86_64": {
|
|
44512
|
+
url: "https://github.com/slackapi/slack-cli/releases/download/v4.2.0/slack_cli_4.2.0_windows_64-bit.zip",
|
|
44513
|
+
archive: "zip",
|
|
44514
|
+
extract: "bin/slack.exe"
|
|
44515
|
+
}
|
|
44516
|
+
}
|
|
44517
|
+
},
|
|
44518
|
+
"telegram-send": {
|
|
44519
|
+
preinstallCloud: true,
|
|
44520
|
+
packages: ["telegram-send==0.39.2"],
|
|
44521
|
+
category: "social",
|
|
44522
|
+
pkg: "telegram-send",
|
|
44523
|
+
installer: "pip",
|
|
44524
|
+
label: "Telegram",
|
|
44525
|
+
summary: "Send Telegram messages, photos and files.",
|
|
44526
|
+
loginCmd: `t=<bot_token>; c=$(curl -sf "https://api.telegram.org/bot$t/getUpdates" | python3 -c "import json,sys; u=[x for x in json.load(sys.stdin)['result'] if 'message' in x]; print(u[-1]['message']['chat']['id'])" 2>/dev/null) || { echo "No message from you yet: open your bot in Telegram, send it any message, then retry"; exit 1; }; mkdir -p "$HOME/.config" && printf '[telegram]\\ntoken = %s\\nchat_id = %s\\n' "$t" "$c" > "$HOME/.config/telegram-send.conf" && telegram-send "Connected to TODOforAI"`,
|
|
44527
|
+
credentialPaths: ["~/.config/telegram-send.conf"],
|
|
44528
|
+
versionCmd: "telegram-send --version 2>/dev/null | sed -n 1p",
|
|
44529
|
+
statusCmd: "test -f ~/.config/telegram-send.conf && echo authenticated",
|
|
44530
|
+
description: "Send messages, send files & images, send video & audio, Markdown/HTML formatting, channel & group support"
|
|
44531
|
+
},
|
|
44532
|
+
"meta-ads": {
|
|
44533
|
+
preinstallCloud: true,
|
|
44534
|
+
packages: ["meta-ads==1.1.0"],
|
|
44535
|
+
category: "marketing",
|
|
44536
|
+
pkg: "meta-ads",
|
|
44537
|
+
installer: "pip",
|
|
44538
|
+
label: "Meta Ads",
|
|
44539
|
+
summary: "Run Facebook and Instagram ad campaigns and see how they perform.",
|
|
44540
|
+
binName: "meta",
|
|
44541
|
+
statusCmd: `out=$(meta auth status 2>&1); rc=$?; printf '%s\\n' "$out" | grep -m1 .; exit $rc`,
|
|
44542
|
+
versionCmd: "meta --version 2>/dev/null | sed -n 1p",
|
|
44543
|
+
description: "Meta/Facebook & Instagram ad campaigns: campaigns/adsets/ads CRUD, insights & reporting, catalog/product-feed/product-set/product-item, ad creatives, datasets (pixels), pages — Marketing API via the official `meta` CLI. No raw/passthrough mode; for arbitrary Graph API calls with a Page token use `facebook-pages-api request <method> <path>`."
|
|
44544
|
+
},
|
|
44545
|
+
"elevenlabs-api": {
|
|
44546
|
+
preinstallCloud: true,
|
|
44547
|
+
category: "media",
|
|
44548
|
+
pkg: "@todoforai/elevenlabs-api",
|
|
44549
|
+
installer: "npm",
|
|
44550
|
+
label: "ElevenLabs",
|
|
44551
|
+
summary: "Turn text into natural speech, in many voices and languages.",
|
|
44552
|
+
versionCmd: "elevenlabs-api --version 2>/dev/null | sed -n 1p",
|
|
44553
|
+
statusCmd: 'test -f ~/.config/todoforai/elevenlabs.json -o -n "$ELEVENLABS_API_KEY" && echo authenticated',
|
|
44554
|
+
loginCmd: "elevenlabs-api auth <api_key>",
|
|
44555
|
+
credentialPaths: ["~/.config/todoforai/elevenlabs.json"],
|
|
44556
|
+
description: "Text-to-speech, voice cloning, multiple languages."
|
|
44557
|
+
},
|
|
44558
|
+
ffmpeg: {
|
|
44559
|
+
commodity: true,
|
|
44560
|
+
category: "media",
|
|
44561
|
+
pkg: "ffmpeg",
|
|
44562
|
+
installer: "system",
|
|
44563
|
+
label: "FFmpeg",
|
|
44564
|
+
summary: "Convert, trim and resize video and audio files.",
|
|
44565
|
+
cloudAptPackages: ["ffmpeg"],
|
|
44566
|
+
preinstallCloud: true,
|
|
44567
|
+
description: "Media swiss-army knife: `ffmpeg -i in.mp4 out.webm`; `ffprobe` for metadata.",
|
|
44568
|
+
versionCmd: "ffmpeg -version 2>/dev/null | sed -n 1p"
|
|
44569
|
+
},
|
|
44570
|
+
"tfa-imagegen": {
|
|
44571
|
+
preinstallCloud: true,
|
|
44572
|
+
category: "media",
|
|
44573
|
+
pkg: "@todoforai/image-api",
|
|
44574
|
+
installer: "npm",
|
|
44575
|
+
label: "Image Gen",
|
|
44576
|
+
summary: "Generate and edit images from a description.",
|
|
44577
|
+
platformAuth: true,
|
|
44578
|
+
versionCmd: "tfa-imagegen --version 2>/dev/null | sed -n 1p",
|
|
44579
|
+
credentialPaths: ["~/.config/todoforai/credentials.json"],
|
|
44580
|
+
description: "AI image generation & editing via TODOFORAI backend (gpt-image, nano-banana, nano-banana-pro, mai-image-2.6)"
|
|
44581
|
+
},
|
|
44582
|
+
"suno-api": {
|
|
44583
|
+
preinstallCloud: true,
|
|
44584
|
+
category: "media",
|
|
44585
|
+
pkg: "@todoforai/suno-api",
|
|
44586
|
+
installer: "npm",
|
|
44587
|
+
label: "Suno",
|
|
44588
|
+
summary: "Generate music and songs from a prompt.",
|
|
44589
|
+
versionCmd: "suno-api --version 2>/dev/null | sed -n 1p",
|
|
44590
|
+
statusCmd: 'test -f ~/.config/todoforai/suno.json -o -n "$SUNO_API_KEY" && echo authenticated',
|
|
44591
|
+
loginCmd: "suno-api auth <api_key>",
|
|
44592
|
+
credentialPaths: [
|
|
44593
|
+
"~/.config/todoforai/suno.json"
|
|
44594
|
+
],
|
|
44595
|
+
description: "AI music generation, custom lyrics, multiple genres."
|
|
44596
|
+
},
|
|
44597
|
+
ntn: {
|
|
44598
|
+
preinstallCloud: true,
|
|
44599
|
+
category: "utility",
|
|
44600
|
+
pkg: "ntn",
|
|
44601
|
+
installer: "npm",
|
|
44602
|
+
label: "Notion",
|
|
44603
|
+
summary: "Read and update Notion pages and databases.",
|
|
44604
|
+
statusCmd: "ntn doctor 2>&1 | grep -E 'Token valid\\s+✔' >/dev/null && echo authenticated",
|
|
44605
|
+
loginCmd: "ntn login",
|
|
44606
|
+
loginNeedsPty: true,
|
|
44607
|
+
oauthUrlPattern: "(https://www\\.notion\\.so/workers/cli-login\\S+)",
|
|
44608
|
+
sessionIdPattern: "verificationCode=([A-Z0-9-]{5,12})",
|
|
44609
|
+
credentialPaths: ["~/.config/notion"],
|
|
44610
|
+
versionCmd: "ntn --version 2>/dev/null | sed -n 1p",
|
|
44611
|
+
description: "Official Notion CLI: raw API calls (`ntn api <path>`), page/data-source management, file uploads, Workers deploy."
|
|
44612
|
+
},
|
|
44613
|
+
"perplexity-api": {
|
|
44614
|
+
preinstallCloud: true,
|
|
44615
|
+
category: "utility",
|
|
44616
|
+
pkg: "@todoforai/perplexity-api",
|
|
44617
|
+
installer: "npm",
|
|
44618
|
+
label: "Perplexity",
|
|
44619
|
+
summary: "Ask questions and get answers researched from the web.",
|
|
44620
|
+
versionCmd: "perplexity-api --version 2>/dev/null | sed -n 1p",
|
|
44621
|
+
statusCmd: 'test -f ~/.config/todoforai/perplexity.json && echo authenticated || test -n "$PERPLEXITY_API_KEY" && echo authenticated',
|
|
44622
|
+
loginCmd: "perplexity-api auth <api_key>",
|
|
44623
|
+
credentialPaths: ["~/.config/todoforai/perplexity.json"],
|
|
44624
|
+
description: "AI-powered web search, chat completions, async chat, embeddings and agent runs"
|
|
44625
|
+
},
|
|
44626
|
+
gh: {
|
|
44627
|
+
category: "development",
|
|
44628
|
+
pkg: "gh",
|
|
44629
|
+
installer: "binary",
|
|
44630
|
+
preinstallCloud: true,
|
|
44631
|
+
label: "GitHub",
|
|
44632
|
+
summary: "Work with your GitHub repos, pull requests and issues.",
|
|
44633
|
+
statusCmd: "gh auth status 2>&1 | grep 'Logged in to' | grep -m1 .",
|
|
44634
|
+
loginCmd: "gh auth login --hostname github.com --git-protocol https",
|
|
44635
|
+
oauthUrlPattern: "(https://github\\.com/login/device\\S*)",
|
|
44636
|
+
deviceCodePattern: "one-time code:\\s*([A-Z0-9]{4}-[A-Z0-9]{4})",
|
|
44637
|
+
canStreamLogin: true,
|
|
44638
|
+
logoutCmd: "gh auth logout --hostname github.com --user {{ACCOUNT}}",
|
|
44639
|
+
credentialPaths: ["~/.config/gh/hosts.yml"],
|
|
44640
|
+
description: "Prefer `gh` over raw GitHub API calls.",
|
|
44641
|
+
versionCmd: "gh --version 2>/dev/null | sed -n 1p",
|
|
44642
|
+
preinstallJarvis: true
|
|
44643
|
+
},
|
|
44644
|
+
mcporter: {
|
|
44645
|
+
category: "development",
|
|
44646
|
+
pkg: "mcporter",
|
|
44647
|
+
installer: "npm",
|
|
44648
|
+
label: "MCP (mcporter)",
|
|
44649
|
+
summary: "Use tools from any MCP server.",
|
|
44650
|
+
statusCmd: "mcporter list 2>/dev/null | grep -v 'No MCP servers' | grep -m1 .",
|
|
44651
|
+
description: "MCP servers as CLI: `mcporter list [server] [--schema]`, `mcporter call <server>.<tool> key=value` (`key=@file`), `mcporter config add <name> <cmd-or-url>`, `mcporter auth <server>`. `--help` for more.",
|
|
44652
|
+
versionCmd: "mcporter --version 2>/dev/null | sed -n 1p",
|
|
44653
|
+
preinstallJarvis: true,
|
|
44654
|
+
preinstallCloud: true
|
|
44655
|
+
},
|
|
44656
|
+
glab: {
|
|
44657
|
+
category: "development",
|
|
44658
|
+
pkg: "glab",
|
|
44659
|
+
installer: "binary",
|
|
44660
|
+
label: "GitLab",
|
|
44661
|
+
summary: "Work with your GitLab repos, merge requests and pipelines.",
|
|
44662
|
+
statusCmd: "glab auth status 2>&1 | grep 'Logged in to' | grep -m1 .",
|
|
44663
|
+
loginCmd: "glab auth login --device",
|
|
44664
|
+
credentialPaths: ["~/.config/glab-cli/config.yml"],
|
|
44665
|
+
description: "GitLab equivalent of `gh`.",
|
|
44666
|
+
versionCmd: "glab --version 2>/dev/null | sed -n 1p",
|
|
44667
|
+
deviceCodePattern: "one-time code: ([A-Z0-9-]+)",
|
|
44668
|
+
oauthUrlPattern: "(https://gitlab\\.com/oauth/device)",
|
|
44669
|
+
canStreamLogin: true
|
|
44670
|
+
},
|
|
44671
|
+
vercel: {
|
|
44672
|
+
preinstallCloud: true,
|
|
44673
|
+
category: "deployment",
|
|
44674
|
+
pkg: "vercel",
|
|
44675
|
+
installer: "npm",
|
|
44676
|
+
label: "Vercel",
|
|
44677
|
+
summary: "Deploy websites to Vercel and manage them.",
|
|
44678
|
+
statusCmd: "vercel whoami 2>/dev/null | tr -d '\\r' | grep -m1 .",
|
|
44679
|
+
loginCmd: "vercel login",
|
|
44680
|
+
credentialPaths: ["~/.local/share/com.vercel.cli/auth.json"],
|
|
44681
|
+
description: "`vercel deploy`; manage env vars, domains, preview URLs.",
|
|
44682
|
+
versionCmd: "vercel --version 2>/dev/null | sed -n 1p",
|
|
44683
|
+
deviceCodePattern: "user_code=([A-Z0-9-]+)",
|
|
44684
|
+
oauthUrlPattern: "(https://vercel\\.com/oauth/device\\S*)",
|
|
44685
|
+
canStreamLogin: true
|
|
44686
|
+
},
|
|
44687
|
+
eas: {
|
|
44688
|
+
category: "deployment",
|
|
44689
|
+
pkg: "eas-cli",
|
|
44690
|
+
installer: "npm",
|
|
44691
|
+
label: "Expo EAS",
|
|
44692
|
+
summary: "Build, update and submit mobile apps with Expo.",
|
|
44693
|
+
statusCmd: `out=$(eas whoami 2>/dev/null) && printf '%s\\n' "$out" | tr -d '\\r' | grep -m1 .`,
|
|
44694
|
+
loginCmd: "eas login --browser",
|
|
44695
|
+
logoutCmd: "eas logout",
|
|
44696
|
+
oauthUrlPattern: "(https://expo\\.dev/(?:sso-)?login\\?\\S+)",
|
|
44697
|
+
canStreamLogin: true,
|
|
44698
|
+
credentialPaths: ["~/.expo/state.json"],
|
|
44699
|
+
description: "Expo Application Services for React Native apps: `eas update --channel <c>` ships a JS-only OTA, `eas build -p ios --profile production --auto-submit` (`-p all` for both) makes a native build and sends it to TestFlight / Play. `eas build:list`, `eas update:list` and `eas submit` inspect or re-submit existing builds. An OTA only reaches installs whose runtimeVersion matches, so a native version bump needs a real build. The browser login binds to a RANDOM loopback port, so it only completes in a browser on the same host; on a remote/headless host set EXPO_TOKEN (Account -> Access tokens) instead. EXPO_TOKEN (Account -> Access tokens) works in CI instead of a login.",
|
|
44700
|
+
versionCmd: "eas --version 2>/dev/null | sed -n 1p"
|
|
44701
|
+
},
|
|
44702
|
+
netlify: {
|
|
44703
|
+
category: "deployment",
|
|
44704
|
+
pkg: "netlify-cli",
|
|
44705
|
+
installer: "npm",
|
|
44706
|
+
label: "Netlify",
|
|
44707
|
+
summary: "Deploy websites to Netlify and manage them.",
|
|
44708
|
+
statusCmd: `netlify api getCurrentUser 2>&1 | sed -n 's/.*"email"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/p' | grep -m10 .`,
|
|
44709
|
+
loginCmd: "netlify login",
|
|
44710
|
+
credentialPaths: ["~/.netlify/config.json"],
|
|
44711
|
+
description: "`netlify deploy --prod` for production; manage env vars, forms, identity.",
|
|
44712
|
+
versionCmd: "netlify --version 2>/dev/null | sed -n 1p",
|
|
44713
|
+
oauthUrlPattern: "(https://app\\.netlify\\.com/authorize\\S+)",
|
|
44714
|
+
canStreamLogin: true
|
|
44715
|
+
},
|
|
44716
|
+
firebase: {
|
|
44717
|
+
preinstallCloud: true,
|
|
44718
|
+
category: "deployment",
|
|
44719
|
+
pkg: "firebase-tools",
|
|
44720
|
+
installer: "npm",
|
|
44721
|
+
label: "Firebase",
|
|
44722
|
+
summary: "Deploy and manage Firebase hosting, database and functions.",
|
|
44723
|
+
statusCmd: "firebase login:list 2>&1 | grep -oiE '[[:alnum:]._%+-]+@[[:alnum:].-]+\\.[[:alpha:]]{2,}' | grep -m1 .",
|
|
44724
|
+
loginCmd: "firebase login --no-localhost --reauth",
|
|
44725
|
+
loginNeedsPty: true,
|
|
44726
|
+
stdinPaste: true,
|
|
44727
|
+
credentialPaths: ["~/.config/configstore/firebase-tools.json"],
|
|
44728
|
+
description: "Deploy hosting/functions, manage Firestore/RTDB rules and data, auth users, emulators.",
|
|
44729
|
+
versionCmd: "firebase --version 2>/dev/null | sed -n 1p",
|
|
44730
|
+
oauthUrlPattern: "(https://auth\\.firebase\\.tools/login\\S+)",
|
|
44731
|
+
sessionIdPattern: "session ID:\\s*([0-9A-Za-z]{4,8})"
|
|
44732
|
+
},
|
|
44733
|
+
wrangler: {
|
|
44734
|
+
preinstallCloud: true,
|
|
44735
|
+
category: "deployment",
|
|
44736
|
+
pkg: "wrangler",
|
|
44737
|
+
installer: "npm",
|
|
44738
|
+
label: "Cloudflare",
|
|
44739
|
+
summary: "Deploy and manage Cloudflare Workers and Pages.",
|
|
44740
|
+
statusCmd: "wrangler whoami 2>&1 | grep -oiE '[[:alnum:]._%+-]+@[[:alnum:].-]+\\.[[:alpha:]]{2,}' | grep -m1 .",
|
|
44741
|
+
loginCmd: "wrangler login",
|
|
44742
|
+
credentialPaths: ["~/.config/.wrangler/config/default.toml"],
|
|
44743
|
+
description: "Tail logs with `wrangler tail`. Use `cloudflared` for tunnels.",
|
|
44744
|
+
versionCmd: "wrangler --version 2>/dev/null | sed -n 1p",
|
|
44745
|
+
oauthUrlPattern: "(https://dash\\.cloudflare\\.com/oauth2/auth\\S+)",
|
|
44746
|
+
oauthCallbackPort: 8976
|
|
44747
|
+
},
|
|
44748
|
+
stripe: {
|
|
44749
|
+
preinstallCloud: true,
|
|
44750
|
+
cloudInstallCmd: "curl -fsSL https://github.com/stripe/stripe-cli/releases/download/v1.50.10/stripe_1.50.10_linux_x86_64.tar.gz | tar -xz -C /usr/local/bin stripe",
|
|
44751
|
+
cloudVerifyCmd: "stripe --version",
|
|
44752
|
+
category: "payments",
|
|
44753
|
+
pkg: "stripe",
|
|
44754
|
+
installer: "binary",
|
|
44755
|
+
label: "Stripe",
|
|
44756
|
+
summary: "Look up Stripe payments, customers and subscriptions.",
|
|
44757
|
+
statusCmd: "stripe config --list 2>/dev/null | sed -n 's/^account_id[[:space:]]*=[[:space:]]*.\\([^'\\'']*\\).*/\\1/p' | grep -m10 .",
|
|
44758
|
+
loginCmd: "stripe login",
|
|
44759
|
+
loginNeedsPty: true,
|
|
44760
|
+
credentialPaths: ["~/.config/stripe/config.toml"],
|
|
44761
|
+
description: "`stripe listen` forwards webhooks locally; `stripe get/post /v1/...` for raw API.",
|
|
44762
|
+
versionCmd: "stripe --version 2>/dev/null | sed -n 1p",
|
|
44763
|
+
deviceCodePattern: "verification code:\\s*([A-Z0-9]{4}-[A-Z0-9]{4})",
|
|
44764
|
+
oauthUrlPattern: "(https://access\\.stripe\\.com/stripecli/\\S+)",
|
|
44765
|
+
canStreamLogin: true
|
|
44766
|
+
},
|
|
44767
|
+
flyctl: {
|
|
44768
|
+
category: "cloud",
|
|
44769
|
+
pkg: "flyctl",
|
|
44770
|
+
installer: "binary",
|
|
44771
|
+
label: "Fly.io",
|
|
44772
|
+
summary: "Deploy and scale apps on Fly.io.",
|
|
44773
|
+
statusCmd: "flyctl auth whoami 2>/dev/null | tr -d '\\r' | grep -m1 .",
|
|
44774
|
+
loginCmd: "flyctl auth login -t <access_token>",
|
|
44775
|
+
credentialPaths: ["~/.fly/config.yml"],
|
|
44776
|
+
description: "`flyctl deploy` from app dir; manage Postgres/volumes/machines/secrets.",
|
|
44777
|
+
versionCmd: "flyctl version 2>/dev/null | sed -n 1p"
|
|
44778
|
+
},
|
|
44779
|
+
supabase: {
|
|
44780
|
+
preinstallCloud: true,
|
|
44781
|
+
cloudInstallCmd: "curl -fsSL https://github.com/supabase/cli/releases/download/v2.117.0/supabase_2.117.0_linux_amd64.tar.gz | tar -xz -C /usr/local/bin supabase",
|
|
44782
|
+
cloudVerifyCmd: "supabase --version",
|
|
44783
|
+
category: "cloud",
|
|
44784
|
+
pkg: "supabase",
|
|
44785
|
+
installer: "binary",
|
|
44786
|
+
label: "Supabase",
|
|
44787
|
+
summary: "Manage your Supabase database, auth and storage.",
|
|
44788
|
+
statusCmd: "supabase projects list >/dev/null 2>&1 && echo authenticated",
|
|
44789
|
+
loginCmd: "supabase login --token <access_token>",
|
|
44790
|
+
credentialPaths: ["~/.config/supabase/access-token"],
|
|
44791
|
+
description: "Link to cloud project; runs/migrates local Postgres.",
|
|
44792
|
+
versionCmd: "supabase --version 2>/dev/null | sed -n 1p"
|
|
44793
|
+
},
|
|
44794
|
+
railway: {
|
|
44795
|
+
category: "cloud",
|
|
44796
|
+
pkg: "@railway/cli",
|
|
44797
|
+
installer: "npm",
|
|
44798
|
+
label: "Railway",
|
|
44799
|
+
summary: "Deploy and manage apps and databases on Railway.",
|
|
44800
|
+
statusCmd: `out=$(RAILWAY_NO_TELEMETRY=1 railway whoami 2>&1); rc=$?; printf '%s\\n' "$out" | grep -m1 .; exit $rc`,
|
|
44801
|
+
loginCmd: "railway login --browserless",
|
|
44802
|
+
credentialPaths: ["~/.railway/config.json"],
|
|
44803
|
+
description: "`railway up` to deploy current dir.",
|
|
44804
|
+
versionCmd: "railway --version 2>/dev/null | sed -n 1p",
|
|
44805
|
+
deviceCodePattern: "user_code=([A-Z0-9-]+)",
|
|
44806
|
+
oauthUrlPattern: "(https://railway\\.com/activate\\S*)",
|
|
44807
|
+
canStreamLogin: true
|
|
44808
|
+
},
|
|
44809
|
+
shopify: {
|
|
44810
|
+
preinstallCloud: true,
|
|
44811
|
+
category: "ecommerce",
|
|
44812
|
+
pkg: "@shopify/cli",
|
|
44813
|
+
installer: "npm",
|
|
44814
|
+
label: "Shopify",
|
|
44815
|
+
summary: "Build and manage Shopify stores and themes.",
|
|
44816
|
+
loginCmd: "shopify auth login --alias tfa",
|
|
44817
|
+
credentialPaths: ["~/.config/shopify-cli-kit-nodejs/config.json"],
|
|
44818
|
+
versionCmd: `node -p "require(require('fs').realpathSync(require('child_process').execSync('command -v shopify',{encoding:'utf8'}).trim()).replace(/\\/bin\\/[^/]*$/, '/package.json')).version" 2>/dev/null`,
|
|
44819
|
+
statusCmd: `python3 -c "import json,os,sys;p=os.path.expanduser('~/.config/shopify-cli-kit-nodejs/config.json');sys.exit(0 if json.load(open(p)).get('session') else 1)" 2>/dev/null && echo authenticated`,
|
|
44820
|
+
deviceCodePattern: "verification code: ([A-Z0-9-]+)",
|
|
44821
|
+
oauthUrlPattern: "(https://accounts\\.shopify\\.com/activate-with-code\\S*)",
|
|
44822
|
+
canStreamLogin: true,
|
|
44823
|
+
description: "Theme development, app scaffolding, store management"
|
|
44824
|
+
},
|
|
44825
|
+
"shop-app": {
|
|
44826
|
+
preinstallCloud: true,
|
|
44827
|
+
category: "ecommerce",
|
|
44828
|
+
pkg: "@todoforai/shop-app",
|
|
44829
|
+
installer: "npm",
|
|
44830
|
+
label: "Shop",
|
|
44831
|
+
summary: "Search products and compare prices across Shopify stores.",
|
|
44832
|
+
description: "Shop.app product search across Shopify stores. `shop-app search '<query>' --ships-to HU -n 10` returns products with price, rating, URL, and variants. `shop-app similar <variantId>` finds lookalikes.",
|
|
44833
|
+
versionCmd: "shop-app --version 2>/dev/null | sed -n 1p"
|
|
44834
|
+
},
|
|
44835
|
+
"datadog-ci": {
|
|
44836
|
+
category: "monitoring",
|
|
44837
|
+
pkg: "@datadog/datadog-ci",
|
|
44838
|
+
installer: "npm",
|
|
44839
|
+
label: "Datadog",
|
|
44840
|
+
summary: "Send test results and releases to Datadog.",
|
|
44841
|
+
versionCmd: "datadog-ci version 2>/dev/null | sed -n 1p",
|
|
44842
|
+
statusCmd: 'test -n "$DATADOG_API_KEY" && echo authenticated',
|
|
44843
|
+
description: "CI test visibility, sourcemap uploads, deployment tracking."
|
|
44844
|
+
},
|
|
44845
|
+
"sentry-cli": {
|
|
44846
|
+
preinstallCloud: true,
|
|
44847
|
+
category: "monitoring",
|
|
44848
|
+
pkg: "@sentry/cli",
|
|
44849
|
+
installer: "npm",
|
|
44850
|
+
label: "Sentry",
|
|
44851
|
+
summary: "Manage Sentry releases and error tracking.",
|
|
44852
|
+
statusCmd: "sentry-cli info --no-defaults >/dev/null 2>&1 && echo authenticated",
|
|
44853
|
+
loginCmd: "sentry-cli login --auth-token <auth_token>",
|
|
44854
|
+
credentialPaths: ["~/.sentryclirc"],
|
|
44855
|
+
description: "`sentry-cli releases new <version>`.",
|
|
44856
|
+
versionCmd: "sentry-cli --version 2>/dev/null | sed -n 1p"
|
|
44857
|
+
},
|
|
44858
|
+
"tfa-cli": {
|
|
44859
|
+
category: "utility",
|
|
44860
|
+
pkg: "@todoforai/cli",
|
|
44861
|
+
installer: "npm",
|
|
44862
|
+
label: "TODOforAI",
|
|
44863
|
+
summary: "Create and follow up on your TODOs, and show results in the chat.",
|
|
44864
|
+
description: 'Manage the user\'s OWN TODOforAI tasks (not code `// TODO` comments). Delegate/spin off work: `tfa-cli "prompt"` creates a real TODO with its own agent and chat (`-n` wait for completion, `--no-watch` fire-and-forget, `--project/--agent/--model`, `--resume <id>`, `addmessage <id> "text"`). Inspect: `list [--status open]`, `--inspect <id>`. Edit any todo without opening it: `status <id> DONE|READY|ARCHIVED|POSTPONED|CANCELLED|DELETED` (close, reopen, shelve, trash — `todo restore <id>` undoes a delete) and `todo set <id|-> title=… group=… star=true schedule=<unix-ms> agent=… brand=…` (own todo fields). Both take ONE id, so a loop over `list --json | jq -r \'.items[].id\'` is how you close, retitle, refile or archive MANY todos at once; `project set|settings|groups …` (name, settings, todo groups), `agent create|update <agent> model=… sysmsg=…` (permissions/MCP/device wiring and delete are refused from an agent shell). Surface results into chat: `show <file|-> [todo-id]` (renders by mimetype; `--card <name>` via project renderer; `--alias <name>` updates in place), `open <url>` embeds a live preview. Notifications: `inbox [needs-you|messages|activity] [--unread]` reads the user\'s bell feed (what finished/failed/needs them while away), `inbox seen [tab]` marks it read; `notify "Title" "message" [--href /t/<id>]` leaves a note in the user\'s Messages tab + phone push — recipient is ALWAYS the key\'s own user (no --to; cannot message teammates or anyone else), shown as from you, same text is delivered once, max 10/day, use for things they must see later (not chat replies). This is the ONLY way to display a local file (image, pdf, html) in the chat: markdown `` with a local/relative path does NOT resolve to workspace files or attachments and 404s in the browser. `--template` for registry workflows.',
|
|
44865
|
+
installCmd: "npm install -g @todoforai/cli",
|
|
44866
|
+
versionCmd: "tfa-cli --version 2>/dev/null | sed -n 1p",
|
|
44867
|
+
preinstallCloud: true,
|
|
44868
|
+
preinstallJarvis: true,
|
|
44869
|
+
aliases: ["todoforai-cli", "todoai"]
|
|
44870
|
+
},
|
|
44871
|
+
curl: {
|
|
44872
|
+
commodity: true,
|
|
44873
|
+
category: "networking",
|
|
44874
|
+
pkg: "curl",
|
|
44875
|
+
installer: "system",
|
|
44876
|
+
label: "Web Request",
|
|
44877
|
+
summary: "Fetch URLs and call web APIs.",
|
|
44878
|
+
binName: "curl",
|
|
44879
|
+
description: "HTTP/HTTPS/FTP client: GET/POST/PUT/DELETE, headers, auth, file upload/download, follow redirects, cookies, TLS.",
|
|
44880
|
+
versionCmd: "curl --version 2>/dev/null | sed -n 1p | cut -d' ' -f1-2",
|
|
44881
|
+
preinstallCloud: true,
|
|
44882
|
+
internal: true,
|
|
44883
|
+
preinstallJarvis: true
|
|
44884
|
+
},
|
|
44885
|
+
cloudflared: {
|
|
44886
|
+
preinstallCloud: true,
|
|
44887
|
+
cloudInstallCmd: "curl -fsSL https://github.com/cloudflare/cloudflared/releases/download/2026.9.0/cloudflared-linux-amd64 -o /usr/local/bin/cloudflared && chmod 0755 /usr/local/bin/cloudflared",
|
|
44888
|
+
cloudVerifyCmd: "cloudflared --version",
|
|
44889
|
+
commodity: true,
|
|
44890
|
+
category: "networking",
|
|
44891
|
+
pkg: "cloudflared",
|
|
44892
|
+
installer: "binary",
|
|
44893
|
+
label: "Tunnel",
|
|
44894
|
+
summary: "Put a local app on a public web address.",
|
|
44895
|
+
description: "`cloudflared tunnel --url http://localhost:<port>` prints a public https URL.",
|
|
44896
|
+
versionCmd: "cloudflared --version 2>/dev/null | sed -n 1p"
|
|
44897
|
+
},
|
|
44898
|
+
vault: {
|
|
44899
|
+
category: "security",
|
|
44900
|
+
pkg: "vault",
|
|
44901
|
+
installer: "binary",
|
|
44902
|
+
label: "HashiCorp Vault",
|
|
44903
|
+
summary: "Read and manage secrets in HashiCorp Vault.",
|
|
44904
|
+
statusCmd: "vault token lookup >/dev/null 2>&1 && echo authenticated",
|
|
44905
|
+
loginCmd: "vault login <token>",
|
|
44906
|
+
credentialPaths: ["~/.vault-token"],
|
|
44907
|
+
versionCmd: "vault --version 2>/dev/null | sed -n '1s/ (.*//p'",
|
|
44908
|
+
description: "HashiCorp Vault secrets management and dynamic credentials."
|
|
44909
|
+
},
|
|
44910
|
+
rclone: {
|
|
44911
|
+
category: "cloud",
|
|
44912
|
+
pkg: "rclone",
|
|
44913
|
+
installer: "binary",
|
|
44914
|
+
preinstallCloud: true,
|
|
44915
|
+
binary: {
|
|
44916
|
+
"linux-x86_64": { url: "https://github.com/todoforai/rclone-backend/releases/download/v0.1.6/rclone-linux-amd64.tar.gz", archive: "tar.gz", extract: "rclone" },
|
|
44917
|
+
"linux-aarch64": { url: "https://github.com/todoforai/rclone-backend/releases/download/v0.1.6/rclone-linux-arm64.tar.gz", archive: "tar.gz", extract: "rclone" },
|
|
44918
|
+
"darwin-x86_64": { url: "https://github.com/todoforai/rclone-backend/releases/download/v0.1.6/rclone-darwin-amd64.tar.gz", archive: "tar.gz", extract: "rclone" },
|
|
44919
|
+
"darwin-aarch64": { url: "https://github.com/todoforai/rclone-backend/releases/download/v0.1.6/rclone-darwin-arm64.tar.gz", archive: "tar.gz", extract: "rclone" },
|
|
44920
|
+
"windows-x86_64": { url: "https://github.com/todoforai/rclone-backend/releases/download/v0.1.6/rclone-windows-amd64.zip", archive: "zip", extract: "rclone.exe" }
|
|
44921
|
+
},
|
|
44922
|
+
label: "Cloud Storage",
|
|
44923
|
+
summary: "Move files between Google Drive, Dropbox, OneDrive, S3 and more.",
|
|
44924
|
+
statusCmd: "rclone listremotes --long 2>/dev/null | tr -s ' ' | grep -v ' todoforai *$' | grep -m20 .",
|
|
44925
|
+
loginCmd: "rclone config",
|
|
44926
|
+
loginTerminalHint: "Connect a storage from its own tile (Google Drive, OneDrive, Dropbox…). For other backends run `rclone config` in a terminal on this host.",
|
|
44927
|
+
oauthUrlPattern: "(https?://127\\.0\\.0\\.1:\\d+/auth\\?\\S+)",
|
|
44928
|
+
oauthCallbackPort: 53682,
|
|
44929
|
+
credentialPaths: ["~/.config/rclone/rclone.conf"],
|
|
44930
|
+
subProviders: {
|
|
44931
|
+
gdrive: {
|
|
44932
|
+
label: "Google Drive",
|
|
44933
|
+
authType: "oauth",
|
|
44934
|
+
connectCmd: "rclone config create {{REMOTE}} drive",
|
|
44935
|
+
remoteName: "gdrive",
|
|
44936
|
+
rcloneType: "drive",
|
|
44937
|
+
userInfoCmd: `TOKEN=$(rclone config dump 2>/dev/null | python3 -c "import json,sys; print(json.loads(json.load(sys.stdin)['{{REMOTE}}']['token'])['access_token'])") && curl -sf 'https://www.googleapis.com/drive/v3/about?fields=user' -H "Authorization: Bearer $TOKEN" | python3 -c "import json,sys; print(json.load(sys.stdin)['user']['emailAddress'])" 2>/dev/null`
|
|
44938
|
+
},
|
|
44939
|
+
onedrive: {
|
|
44940
|
+
label: "OneDrive",
|
|
44941
|
+
authType: "oauth",
|
|
44942
|
+
connectCmd: "rclone config create {{REMOTE}} onedrive",
|
|
44943
|
+
remoteName: "onedrive",
|
|
44944
|
+
rcloneType: "onedrive",
|
|
44945
|
+
userInfoCmd: `TOKEN=$(rclone config dump 2>/dev/null | python3 -c "import json,sys; print(json.loads(json.load(sys.stdin)['{{REMOTE}}']['token'])['access_token'])") && curl -sf 'https://graph.microsoft.com/v1.0/me' -H "Authorization: Bearer $TOKEN" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('mail') or d.get('userPrincipalName',''))" 2>/dev/null`
|
|
44946
|
+
},
|
|
44947
|
+
dropbox: {
|
|
44948
|
+
label: "Dropbox",
|
|
44949
|
+
authType: "oauth",
|
|
44950
|
+
connectCmd: "rclone config create {{REMOTE}} dropbox",
|
|
44951
|
+
remoteName: "dropbox",
|
|
44952
|
+
rcloneType: "dropbox",
|
|
44953
|
+
userInfoCmd: `TOKEN=$(rclone config dump 2>/dev/null | python3 -c "import json,sys; print(json.loads(json.load(sys.stdin)['{{REMOTE}}']['token'])['access_token'])") && curl -sf -X POST 'https://api.dropboxapi.com/2/users/get_current_account' -H "Authorization: Bearer $TOKEN" | python3 -c "import json,sys; print(json.load(sys.stdin)['email'])" 2>/dev/null`
|
|
44954
|
+
},
|
|
44955
|
+
s3: {
|
|
44956
|
+
label: "Amazon S3",
|
|
44957
|
+
authType: "credentials",
|
|
44958
|
+
connectCmd: "rclone config create {{REMOTE}} s3",
|
|
44959
|
+
remoteName: "s3",
|
|
44960
|
+
rcloneType: "s3"
|
|
44961
|
+
}
|
|
44962
|
+
},
|
|
44963
|
+
versionCmd: "rclone version 2>/dev/null | sed -n 1p",
|
|
44964
|
+
preinstallJarvis: true,
|
|
44965
|
+
description: "Access Google Drive, OneDrive, Dropbox, S3, and 40+ cloud providers. List, copy, sync, move, and mount files; use `rclone config create <name> <provider>` to connect."
|
|
44966
|
+
},
|
|
44967
|
+
aws: {
|
|
44968
|
+
preinstallCloud: true,
|
|
44969
|
+
cloudInstallCmd: "curl -fsSL https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip -o /tmp/awscli.zip && unzip -qo /tmp/awscli.zip -d /tmp && /tmp/aws/install --update && rm -rf /tmp/aws /tmp/awscli.zip",
|
|
44970
|
+
cloudVerifyCmd: "aws --version",
|
|
44971
|
+
category: "cloud",
|
|
44972
|
+
pkg: "awscli",
|
|
44973
|
+
installer: "system",
|
|
44974
|
+
label: "AWS",
|
|
44975
|
+
summary: "Manage your AWS account and services.",
|
|
44976
|
+
installCmd: `mkdir -p "$HOME/.todoforai/tools/bin"; if [ "$(uname -s)" = Darwin ]; then curl -fsSL https://awscli.amazonaws.com/AWSCLIV2.pkg -o /tmp/AWSCLIV2.pkg && printf '<?xml version="1.0" encoding="UTF-8"?>\\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\\n<plist version="1.0"><array><dict><key>choiceAttribute</key><string>customLocation</string><key>attributeSetting</key><string>%s</string><key>choiceIdentifier</key><string>default</string></dict></array></plist>\\n' "$HOME/.todoforai/tools" > /tmp/aws-choices.xml && installer -pkg /tmp/AWSCLIV2.pkg -target CurrentUserHomeDirectory -applyChoiceChangesXML /tmp/aws-choices.xml && mkdir -p "$HOME/.todoforai/tools/bin" && ln -sf "$HOME/.todoforai/tools/aws-cli/aws" "$HOME/.todoforai/tools/bin/aws" && ln -sf "$HOME/.todoforai/tools/aws-cli/aws_completer" "$HOME/.todoforai/tools/bin/aws_completer"; rc=$?; rm -f /tmp/AWSCLIV2.pkg /tmp/aws-choices.xml; exit $rc; else ARCH=$(uname -m); case "$ARCH" in arm64|aarch64) ARCH=aarch64;; *) ARCH=x86_64;; esac; curl -fsSL "https://awscli.amazonaws.com/awscli-exe-linux-$ARCH.zip" -o /tmp/awscli.zip && rm -rf /tmp/aws && unzip -qo /tmp/awscli.zip -d /tmp && /tmp/aws/install --update --install-dir "$HOME/.todoforai/tools/aws-cli" --bin-dir "$HOME/.todoforai/tools/bin"; rc=$?; rm -rf /tmp/aws /tmp/awscli.zip; exit $rc; fi`,
|
|
44977
|
+
statusCmd: "aws sts get-caller-identity --query Arn --output text 2>/dev/null | sed 's/.*\\///' | grep -m1 .",
|
|
44978
|
+
loginCmd: "aws configure set aws_access_key_id <access_key_id> && aws configure set aws_secret_access_key <secret_access_key> && aws configure set region <region>",
|
|
44979
|
+
credentialPaths: ["~/.aws/credentials", "~/.aws/config", "~/.aws/sso/cache"],
|
|
44980
|
+
description: "Official AWS CLI v2, installed user-locally into ~/.todoforai/tools (no sudo; Linux x86_64/aarch64 + macOS). Auth via `aws configure` (access keys) or `aws configure sso` + `aws sso login` (browser device flow).",
|
|
44981
|
+
versionCmd: "aws --version 2>/dev/null | sed -n 1p",
|
|
44982
|
+
uninstallCmd: 'rm -rf "$HOME/.todoforai/tools/aws-cli" "$HOME/.todoforai/tools/bin/aws" "$HOME/.todoforai/tools/bin/aws_completer"; hash -r 2>/dev/null; ! command -v aws >/dev/null 2>&1 || { echo "aws is still on PATH: $(command -v aws) — not installed by TODOforAI, remove it with the package manager that installed it (snap/apt/brew/sudo)"; exit 1; }'
|
|
44983
|
+
},
|
|
44984
|
+
gcloud: {
|
|
44985
|
+
category: "cloud",
|
|
44986
|
+
pkg: "google-cloud-sdk",
|
|
44987
|
+
installer: "system",
|
|
44988
|
+
label: "Google Cloud",
|
|
44989
|
+
summary: "Manage your Google Cloud projects and services.",
|
|
44990
|
+
installCmd: 'curl -sSL https://sdk.cloud.google.com | bash -s -- --disable-prompts && mkdir -p "$HOME/.todoforai/tools/bin" && ln -sf "$HOME/google-cloud-sdk/bin/gcloud" "$HOME/.todoforai/tools/bin/gcloud"',
|
|
44991
|
+
statusCmd: `gcloud auth list --filter=status:ACTIVE --format="value(account)" 2>/dev/null | tr -d '\\r' | grep -m1 .`,
|
|
44992
|
+
loginCmd: "gcloud auth login --no-launch-browser",
|
|
44993
|
+
loginNeedsPty: true,
|
|
44994
|
+
stdinPaste: true,
|
|
44995
|
+
oauthUrlPattern: "(https://accounts\\.google\\.com/o/oauth2/auth\\S+)",
|
|
44996
|
+
credentialPaths: ["~/.config/gcloud"],
|
|
44997
|
+
description: "Official Google Cloud CLI. `gcloud auth login --no-launch-browser` prints the OAuth URL and waits for the pasted verification code.",
|
|
44998
|
+
versionCmd: "gcloud --version 2>/dev/null | sed -n 1p",
|
|
44999
|
+
uninstallCmd: 'rm -rf "$HOME/google-cloud-sdk" "$HOME/.todoforai/tools/bin/gcloud"; hash -r 2>/dev/null; ! command -v gcloud >/dev/null 2>&1 || { echo "gcloud is still on PATH: $(command -v gcloud)"; exit 1; }',
|
|
45000
|
+
updateCmd: "gcloud components update --quiet"
|
|
45001
|
+
},
|
|
45002
|
+
az: {
|
|
45003
|
+
category: "cloud",
|
|
45004
|
+
pkg: "azure-cli",
|
|
45005
|
+
installer: "pip",
|
|
45006
|
+
label: "Azure",
|
|
45007
|
+
summary: "Manage your Microsoft Azure resources.",
|
|
45008
|
+
binName: "az",
|
|
45009
|
+
statusCmd: "az account show --query user.name -o tsv 2>/dev/null | tr -d '\\r' | grep -m1 .",
|
|
45010
|
+
loginCmd: "az login --use-device-code",
|
|
45011
|
+
oauthUrlPattern: "(https://login\\.microsoft\\.com/device\\S*)",
|
|
45012
|
+
deviceCodePattern: "enter the code ([A-Z0-9]+)",
|
|
45013
|
+
canStreamLogin: true,
|
|
45014
|
+
credentialPaths: ["~/.azure"],
|
|
45015
|
+
description: "Official Azure CLI. Device-code login: prints login.microsoft.com/device + a code, then polls — no callback port.",
|
|
45016
|
+
versionCmd: `az version --query '"azure-cli"' -o tsv 2>/dev/null | sed -n 1p`
|
|
45017
|
+
},
|
|
45018
|
+
doctl: {
|
|
45019
|
+
preinstallCloud: true,
|
|
45020
|
+
cloudInstallCmd: "curl -fsSL https://github.com/digitalocean/doctl/releases/download/v1.168.0/doctl-1.168.0-linux-amd64.tar.gz | tar -xz -C /usr/local/bin doctl",
|
|
45021
|
+
cloudVerifyCmd: "doctl version",
|
|
45022
|
+
category: "cloud",
|
|
45023
|
+
pkg: "doctl",
|
|
45024
|
+
installer: "binary",
|
|
45025
|
+
label: "DigitalOcean",
|
|
45026
|
+
summary: "Manage your DigitalOcean servers and apps.",
|
|
45027
|
+
statusCmd: "doctl account get --format Email --no-header 2>/dev/null | tr -d '\\r' | grep -m1 .",
|
|
45028
|
+
loginCmd: "doctl auth init -t <access_token>",
|
|
45029
|
+
loginNeedsPty: true,
|
|
45030
|
+
credentialPaths: ["~/.config/doctl/config.yaml"],
|
|
45031
|
+
description: "Official DigitalOcean CLI. `doctl auth init` prompts for an API token from cloud.digitalocean.com/account/api/tokens.",
|
|
45032
|
+
versionCmd: "doctl version 2>/dev/null | sed -n 1p"
|
|
45033
|
+
},
|
|
45034
|
+
rdt: {
|
|
45035
|
+
preinstallCloud: true,
|
|
45036
|
+
packages: ["rdt-cli==0.4.1"],
|
|
45037
|
+
category: "social",
|
|
45038
|
+
pkg: "rdt-cli",
|
|
45039
|
+
installer: "pip",
|
|
45040
|
+
label: "Reddit",
|
|
45041
|
+
summary: "Search Reddit, read threads and post comments.",
|
|
45042
|
+
statusCmd: `rdt status 2>&1 | sed -n 's/.*"username"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/p' | grep -m10 .`,
|
|
45043
|
+
loginCmd: "rdt login",
|
|
45044
|
+
loginTerminalHint: "Log in to reddit.com in a browser on this host (Chrome/Firefox), then run `rdt login`: it imports that browser's Reddit cookies.",
|
|
45045
|
+
credentialPaths: ["~/.config/rdt-cli/credential.json"],
|
|
45046
|
+
description: 'Reddit CLI. Read: `rdt search "<query>" -r <sub> --json -c`, `rdt sub <name>`, `rdt read <postId>`, `rdt user-comments <user>`. Write: `rdt comment <postId> "<text>"`, `rdt upvote`, `rdt save`.',
|
|
45047
|
+
versionCmd: "rdt --version 2>/dev/null | sed -n 1p"
|
|
45048
|
+
},
|
|
45049
|
+
pymupdf: {
|
|
45050
|
+
category: "utility",
|
|
45051
|
+
pkg: "pymupdf",
|
|
45052
|
+
packages: ["pymupdf==1.28.2"],
|
|
45053
|
+
installer: "pip",
|
|
45054
|
+
label: "PDF",
|
|
45055
|
+
summary: "Read, edit, merge and split PDF files.",
|
|
45056
|
+
versionCmd: "{ python -c 'import pymupdf; print(pymupdf.__version__)' || python3 -c 'import pymupdf; print(pymupdf.__version__)' || py -c 'import pymupdf; print(pymupdf.__version__)'; } 2>/dev/null",
|
|
45057
|
+
preinstallCloud: true,
|
|
45058
|
+
cloudVerifyCmd: "python3 -c 'import pymupdf'",
|
|
45059
|
+
preinstallJarvis: true,
|
|
45060
|
+
description: "PDF text editing, extraction, merge, split. Call via `python3 -c`."
|
|
45061
|
+
},
|
|
45062
|
+
pyhanko: {
|
|
45063
|
+
category: "security",
|
|
45064
|
+
pkg: "pyhanko-cli",
|
|
45065
|
+
packages: ["pyhanko-cli==0.5.0", "pyHanko[pkcs11,image-support,opentype]==0.37.0"],
|
|
45066
|
+
installer: "pip",
|
|
45067
|
+
binName: "pyhanko",
|
|
45068
|
+
label: "PDF Signing",
|
|
45069
|
+
summary: "Digitally sign and verify PDFs with a certificate (PAdES, timestamps, smart cards).",
|
|
45070
|
+
versionCmd: "pyhanko --version 2>/dev/null | sed -n 1p",
|
|
45071
|
+
preinstallCloud: true,
|
|
45072
|
+
cloudVerifyCmd: "pyhanko --version",
|
|
45073
|
+
preinstallJarvis: true,
|
|
45074
|
+
description: "Cryptographic PDF signing/validation (PAdES B-B..B-LTA). Sign: `pyhanko sign addsig --field Sig1 [--timestamp-url <tsa>] pkcs12 --passfile pass.txt in.pdf out.pdf cert.p12`; smart card/eID: `... pkcs11 --lib <pkcs11.so> in.pdf out.pdf`; verify: `pyhanko sign validate --pretty-print [--trust ca.pem] out.pdf`; list fields: `pyhanko sign list`. The signer's cert/key comes from the user (vault); self-signed certs validate only with `--trust`. Not for click-to-sign workflows."
|
|
45075
|
+
},
|
|
45076
|
+
anydoc: {
|
|
45077
|
+
category: "utility",
|
|
45078
|
+
pkg: "@firecrawl/anydoc",
|
|
45079
|
+
installer: "npm",
|
|
45080
|
+
binName: "anydoc",
|
|
45081
|
+
label: "Doc to Markdown",
|
|
45082
|
+
summary: "Turn Word, PowerPoint, Excel and other documents into clean text.",
|
|
45083
|
+
description: "Office/rich docs (docx/pptx/xlsx/odt/rtf/epub/csv) → Markdown, fully local. `anydoc file.docx` → stdout, `-o out.md` writes, `anydoc - --format csv < stdin`. Prefer over raw XML extraction for reading; one-way, no OCR for scanned PDFs.",
|
|
45084
|
+
versionCmd: "anydoc --version 2>/dev/null | sed -n 1p",
|
|
45085
|
+
preinstallCloud: true,
|
|
45086
|
+
preinstallJarvis: true
|
|
45087
|
+
},
|
|
45088
|
+
"agent-browser": {
|
|
45089
|
+
category: "development",
|
|
45090
|
+
pkg: "agent-browser@0.30.1",
|
|
45091
|
+
installer: "npm",
|
|
45092
|
+
preinstallCloud: true,
|
|
45093
|
+
label: "Browser",
|
|
45094
|
+
summary: "Browse websites automatically: click, type, read and screenshot.",
|
|
45095
|
+
description: "Browser automation over CDP (`open`/`snapshot`/`click`/`type`/`fill`/`read`/`screenshot`/`eval`). `connect '<url>'` to a listed live browser (prefer the user's real Chrome), verify with `snapshot`; if empty, `close` (own session only) and reconnect once. NEVER `close --all`. Shared browser: `tab new` for your own tab, `tab <id>` before each command. No URL listed: cloud → browser-manager-cli; PC with display → Chrome `--remote-debugging-port=9222`, attach `--cdp 9222`.",
|
|
45096
|
+
versionCmd: "agent-browser --version 2>/dev/null | sed -n 1p",
|
|
45097
|
+
preinstallJarvis: true
|
|
45098
|
+
},
|
|
45099
|
+
"browser-manager-cli": {
|
|
45100
|
+
category: "development",
|
|
45101
|
+
pkg: "browser-manager-cli",
|
|
45102
|
+
installer: "binary",
|
|
45103
|
+
binName: "browser-manager-cli",
|
|
45104
|
+
preinstallCloud: true,
|
|
45105
|
+
label: "Browser Manager",
|
|
45106
|
+
summary: "Start browsers in the cloud for automated browsing.",
|
|
45107
|
+
platformAuth: true,
|
|
45108
|
+
description: "Spawns cloud Chromium sessions, each exposed as a CDP WebSocket for agent-browser to drive. `create` prints one line ending in a cdpUrl — pass it straight to `agent-browser connect '<cdpUrl>'` (connect the cdpUrl, not the Chrome debug port). `list`/`get <id>`, `delete <id>`/`delete-all`, `hibernate`/`restore`. Sessions run stealth Chromium that reads as a real desktop browser. For sites that block datacenter IPs, route through a proxy: `create --proxy socks5://host:port [--proxy-user U --proxy-pass P]`. Use this on the cloud VM instead of launching a local browser.",
|
|
45109
|
+
versionCmd: "browser-manager-cli version 2>/dev/null | sed -n 1p",
|
|
45110
|
+
binary: {
|
|
45111
|
+
"linux-x86_64": {
|
|
45112
|
+
url: "https://github.com/todoforai/browser-manager/releases/download/cli-v0.1.1/browser-manager-cli-linux-x86_64",
|
|
45113
|
+
archive: "raw"
|
|
45114
|
+
},
|
|
45115
|
+
"linux-aarch64": {
|
|
45116
|
+
url: "https://github.com/todoforai/browser-manager/releases/download/cli-v0.1.1/browser-manager-cli-linux-aarch64",
|
|
45117
|
+
archive: "raw"
|
|
45118
|
+
},
|
|
45119
|
+
"darwin-x86_64": {
|
|
45120
|
+
url: "https://github.com/todoforai/browser-manager/releases/download/cli-v0.1.1/browser-manager-cli-darwin-x86_64",
|
|
45121
|
+
archive: "raw"
|
|
45122
|
+
},
|
|
45123
|
+
"darwin-aarch64": {
|
|
45124
|
+
url: "https://github.com/todoforai/browser-manager/releases/download/cli-v0.1.1/browser-manager-cli-darwin-aarch64",
|
|
45125
|
+
archive: "raw"
|
|
45126
|
+
},
|
|
45127
|
+
"windows-x86_64": {
|
|
45128
|
+
url: "https://github.com/todoforai/browser-manager/releases/download/cli-v0.1.1/browser-manager-cli-windows-x86_64.exe",
|
|
45129
|
+
archive: "raw"
|
|
45130
|
+
}
|
|
45131
|
+
},
|
|
45132
|
+
preinstallJarvis: true
|
|
45133
|
+
},
|
|
45134
|
+
"todoforai-browser": {
|
|
45135
|
+
category: "development",
|
|
45136
|
+
pkg: "@todoforai/browser",
|
|
45137
|
+
aliases: ["tfa-browser"],
|
|
45138
|
+
installer: "npm",
|
|
45139
|
+
preinstallCloud: true,
|
|
45140
|
+
label: "Browser (Extension)",
|
|
45141
|
+
summary: "Drive your own browser through the TODOforAI extension.",
|
|
45142
|
+
versionCmd: `node -p "require(require('fs').realpathSync(require('child_process').execSync('command -v todoforai-browser',{encoding:'utf8'}).trim()).replace(/\\/dist\\/index\\.js$/, '/package.json')).version" 2>/dev/null`,
|
|
45143
|
+
internal: true,
|
|
45144
|
+
preinstallJarvis: true,
|
|
45145
|
+
description: "Browser automation via extension (non-headless), web scraping, accessibility tree snapshots, screenshots, PDF generation"
|
|
45146
|
+
},
|
|
45147
|
+
"tfa-explore": {
|
|
45148
|
+
preinstallCloud: true,
|
|
45149
|
+
category: "development",
|
|
45150
|
+
pkg: "@todoforai/tfa-explore",
|
|
45151
|
+
installer: "npm",
|
|
45152
|
+
label: "Explore",
|
|
45153
|
+
summary: "Send a helper agent to explore a codebase and report back.",
|
|
45154
|
+
platformAuth: true,
|
|
45155
|
+
description: 'Sub-agent codebase exploration in its own context, you get only the answer: `tfa-explore "where is auth handled?" [--repo <path>]`. Prefer over manual multi-file searching.',
|
|
45156
|
+
versionCmd: "tfa-explore --version 2>/dev/null | sed -n 1p",
|
|
45157
|
+
installCmd: "npm install -g @todoforai/tfa-explore",
|
|
45158
|
+
internal: true
|
|
45159
|
+
},
|
|
45160
|
+
"tfa-review": {
|
|
45161
|
+
preinstallCloud: true,
|
|
45162
|
+
category: "development",
|
|
45163
|
+
pkg: "@todoforai/tfa-review",
|
|
45164
|
+
installer: "npm",
|
|
45165
|
+
label: "Review",
|
|
45166
|
+
summary: "Send a helper agent to review your code changes.",
|
|
45167
|
+
platformAuth: true,
|
|
45168
|
+
description: 'Sub-agent review of a git diff in its own context: `tfa-review "<goal>" [--repo <path>] [--against <ref>]` (default: uncommitted). Run before calling a non-trivial change done.',
|
|
45169
|
+
versionCmd: "tfa-review --version 2>/dev/null | sed -n 1p",
|
|
45170
|
+
installCmd: "npm install -g @todoforai/tfa-review",
|
|
45171
|
+
internal: true
|
|
45172
|
+
},
|
|
45173
|
+
"tfa-summary": {
|
|
45174
|
+
preinstallCloud: true,
|
|
45175
|
+
category: "development",
|
|
45176
|
+
pkg: "@todoforai/tfa-summary",
|
|
45177
|
+
installer: "npm",
|
|
45178
|
+
label: "Summary",
|
|
45179
|
+
summary: "Summarize files or long output with a helper agent.",
|
|
45180
|
+
platformAuth: true,
|
|
45181
|
+
description: 'Sub-agent summary so raw text never enters your context: `cat big.log | tfa-summary "what failed?"`, or `-f <file>` (repeatable).',
|
|
45182
|
+
versionCmd: "tfa-summary --version 2>/dev/null | sed -n 1p",
|
|
45183
|
+
installCmd: "npm install -g @todoforai/tfa-summary",
|
|
45184
|
+
internal: true
|
|
45185
|
+
},
|
|
45186
|
+
"tfa-handoff": {
|
|
45187
|
+
preinstallCloud: true,
|
|
45188
|
+
category: "development",
|
|
45189
|
+
pkg: "@todoforai/tfa-handoff",
|
|
45190
|
+
installer: "npm",
|
|
45191
|
+
label: "Handoff",
|
|
45192
|
+
summary: "Turn a long conversation into a short handover note.",
|
|
45193
|
+
platformAuth: true,
|
|
45194
|
+
description: "Compact a conversation into a handoff document for a fresh agent.",
|
|
45195
|
+
versionCmd: "tfa-handoff --version 2>/dev/null | sed -n 1p",
|
|
45196
|
+
installCmd: "npm install -g @todoforai/tfa-handoff",
|
|
45197
|
+
internal: true
|
|
45198
|
+
},
|
|
45199
|
+
"tfa-wait": {
|
|
45200
|
+
category: "development",
|
|
45201
|
+
pkg: "@todoforai/tfa-wait",
|
|
45202
|
+
installer: "npm",
|
|
45203
|
+
label: "Wait",
|
|
45204
|
+
summary: "Wait for a time or an event, then continue the task.",
|
|
45205
|
+
platformAuth: true,
|
|
45206
|
+
description: "Schedule a wake-up: `tfa-wait '<expr>' \"<message>\"` — expr is a time (`3h`, `daily@9:00`), a webhook, or a command's output; the message arrives later as a new chat message. Wait for a long job: `tfa-wait 'bench || 6h' \"Done: exit {{payload.exit}}\"` then `tfa-wait run '<cmd>' --pipe bench --on-exit &` (each stdout line = event; cancelling kills the command). Never pgrep/sleep poll loops.",
|
|
45207
|
+
versionCmd: "tfa-wait --version 2>/dev/null | sed -n 1p",
|
|
45208
|
+
installCmd: "npm install -g @todoforai/tfa-wait",
|
|
45209
|
+
preinstallCloud: true,
|
|
45210
|
+
internal: true,
|
|
45211
|
+
preinstallJarvis: true
|
|
45212
|
+
},
|
|
45213
|
+
"tfa-vault": {
|
|
45214
|
+
category: "security",
|
|
45215
|
+
pkg: "@todoforai/tfa-vault",
|
|
45216
|
+
installer: "npm",
|
|
45217
|
+
label: "Vault",
|
|
45218
|
+
summary: "Your password and secret store.",
|
|
45219
|
+
platformAuth: true,
|
|
45220
|
+
description: "The user's encrypted secret vault — using its secrets is the intended workflow. READ: when a task needs a key/token/password the user owns, `tfa-vault list` then `get <path>`; don't ask for what's already stored; never echo values. WRITE proactively: any new secret in the conversation (pasted, generated, from a login) → `put <path> field=value` (`patch` adds fields; `put` refuses an existing path — `put --force` replaces all fields on rotation). Paths: `accounts/<svc>`, `api/<svc>`, `secrets/<name>`.",
|
|
45221
|
+
versionCmd: "tfa-vault --version 2>/dev/null | sed -n 1p",
|
|
45222
|
+
installCmd: "npm install -g @todoforai/tfa-vault",
|
|
45223
|
+
preinstallCloud: true,
|
|
45224
|
+
preinstallJarvis: true
|
|
45225
|
+
},
|
|
45226
|
+
"tfa-settings": {
|
|
45227
|
+
category: "utility",
|
|
45228
|
+
pkg: "@todoforai/tfa-settings",
|
|
45229
|
+
installer: "npm",
|
|
45230
|
+
label: "Settings",
|
|
45231
|
+
summary: "Change your TODOforAI app preferences from the shell.",
|
|
45232
|
+
platformAuth: true,
|
|
45233
|
+
description: "The user's TODOforAI app preferences (AppSettings): `list`, `get <key>`, `set key=value …`, `keys` (documented keys). Use when asked to change theme, list style, tool-block density, thinking traces, sounds/toasts, preview columns, wallpaper etc. Values are JSON when parseable. Consent/credential/escalation keys (shellExecution, notifications, analytics, keyboard bindings, vault provider, billing address) are refused from an agent shell — tell the user to change those in Settings.",
|
|
45234
|
+
versionCmd: "tfa-settings --version 2>/dev/null | sed -n 1p",
|
|
45235
|
+
installCmd: "npm install -g @todoforai/tfa-settings",
|
|
45236
|
+
preinstallCloud: true,
|
|
45237
|
+
preinstallJarvis: true
|
|
45238
|
+
},
|
|
45239
|
+
"tfa-memory": {
|
|
45240
|
+
category: "productivity",
|
|
45241
|
+
pkg: "@todoforai/tfa-memory",
|
|
45242
|
+
installer: "npm",
|
|
45243
|
+
label: "Memory",
|
|
45244
|
+
summary: "Your long-term memory of notes, facts and past TODOs.",
|
|
45245
|
+
platformAuth: true,
|
|
45246
|
+
description: 'User\'s long-term memory (notes/procedures/decisions, address `source/ref`) plus every past TODO message (read-only, `todo/<todoId>:<messageId>`). READ: `search "<topic>" --mode fused` first (`lexical` = all words newest first, `semantic` = meaning; `--source todo` / `--todo <id>` narrow). stderr prints `N row(s) contain a query word` — if N exceeds what you saw, page with `--exclude <address>`. `get <address>` = full row, `around <address> --before N --after N` = surrounding dialogue. Before answering from memory, search once more with your draft\'s key facts. WRITE durable facts proactively: `add <source>/<ref> "text"` (same address updates in place; short source names like deploy-notes, preferences). `sources` lists them.',
|
|
45247
|
+
versionCmd: "tfa-memory --version 2>/dev/null | sed -n 1p",
|
|
45248
|
+
installCmd: "npm install -g @todoforai/tfa-memory",
|
|
45249
|
+
preinstallCloud: true,
|
|
45250
|
+
preinstallJarvis: true
|
|
45251
|
+
},
|
|
45252
|
+
"todoregistry-cli": {
|
|
45253
|
+
category: "development",
|
|
45254
|
+
pkg: "@todoforai/todoregistry-cli",
|
|
45255
|
+
installer: "npm",
|
|
45256
|
+
label: "TODO Registry",
|
|
45257
|
+
summary: "Browse ready-made business task templates.",
|
|
45258
|
+
description: "Reusable business task templates (SEO, email, CRM, ads, social, …): `search <q>`, `get <id>`, `list [category]`, `categories`, `create --name … --description … --body @prompt.md`. To recommend a template, don't create a todo directly — `tfa-cli recommend --template <id> --project <projectId>`.",
|
|
45259
|
+
versionCmd: "todoregistry-cli --version 2>/dev/null | sed -n 1p",
|
|
45260
|
+
installCmd: "npm install -g @todoforai/todoregistry-cli",
|
|
45261
|
+
preinstallCloud: true,
|
|
45262
|
+
internal: true,
|
|
45263
|
+
preinstallJarvis: true
|
|
45264
|
+
},
|
|
45265
|
+
"tfa-surface": {
|
|
45266
|
+
preinstallCloud: true,
|
|
45267
|
+
category: "utility",
|
|
45268
|
+
pkg: "@todoforai/tfa-surface",
|
|
45269
|
+
installer: "npm",
|
|
45270
|
+
label: "Surface",
|
|
45271
|
+
summary: "See and control what is on your TODOforAI screen.",
|
|
45272
|
+
platformAuth: true,
|
|
45273
|
+
description: "The user's live UI from the shell. Everything on screen is a SURFACE holding one ref (a todo id, `<todoId>:<alias>` = a shown artifact, or a url). `describe` lists them (kind, id, ref, focus, embedded-browser CDP URL; `--json`). `open overlay <todoId>` (full page), `open window <ref>` (floating), `open panel changes|preview|browser|terminal [--url]` (a todo's side panel), `open card <ref> [--title] [--group <slug>]` (a persisted board card every member sees); `close|focus <kind> <id>`. Persistent state (plans, reports, live status) → `show chat.panel <todoId> \"...md\"` (stdin ok; re-show updates in place), `clear` removes; events go in chat. `card add|list|get|rm <name>` = project HTML renderers. Exit 2 = no frontend connected.",
|
|
45274
|
+
versionCmd: "tfa-surface --version 2>/dev/null | sed -n 1p",
|
|
45275
|
+
installCmd: "npm install -g @todoforai/tfa-surface",
|
|
45276
|
+
internal: true
|
|
45277
|
+
},
|
|
45278
|
+
bun: {
|
|
45279
|
+
commodity: true,
|
|
45280
|
+
category: "development",
|
|
45281
|
+
pkg: "bun",
|
|
45282
|
+
installer: "system",
|
|
45283
|
+
label: "JS/TS Runtime",
|
|
45284
|
+
summary: "Run JavaScript and TypeScript code, fast.",
|
|
45285
|
+
versionCmd: "bun --version 2>/dev/null",
|
|
45286
|
+
preinstallCloud: true,
|
|
45287
|
+
cloudInstallCmd: "export BUN_INSTALL=/usr/local && curl -fsSL https://bun.sh/install | bash && ln -sf /usr/local/bin/bun /usr/local/bin/bunx",
|
|
45288
|
+
cloudVerifyCmd: "bun --version",
|
|
45289
|
+
internal: true,
|
|
45290
|
+
preinstallJarvis: true,
|
|
45291
|
+
description: "Run JavaScript/TypeScript, install packages, run tests (`bun`, `bunx`)."
|
|
45292
|
+
},
|
|
45293
|
+
bunx: {
|
|
45294
|
+
commodity: true,
|
|
45295
|
+
category: "development",
|
|
45296
|
+
pkg: "bunx",
|
|
45297
|
+
installer: "system",
|
|
45298
|
+
label: "Bun Package Runner",
|
|
45299
|
+
summary: "Run a JavaScript package without installing it.",
|
|
45300
|
+
versionCmd: "bunx --version 2>/dev/null",
|
|
45301
|
+
preinstallCloud: true,
|
|
45302
|
+
cloudVerifyCmd: "bunx --version",
|
|
45303
|
+
internal: true,
|
|
45304
|
+
description: "Run package binaries with Bun."
|
|
45305
|
+
},
|
|
45306
|
+
node: {
|
|
45307
|
+
commodity: true,
|
|
45308
|
+
category: "development",
|
|
45309
|
+
pkg: "node",
|
|
45310
|
+
installer: "system",
|
|
45311
|
+
label: "JavaScript Runtime",
|
|
45312
|
+
summary: "Run JavaScript code.",
|
|
45313
|
+
versionCmd: "node -p 'process.version' 2>/dev/null",
|
|
45314
|
+
preinstallCloud: true,
|
|
45315
|
+
cloudInstallCmd: "curl -fsSL https://deb.nodesource.com/setup_24.x | bash -",
|
|
45316
|
+
cloudAptPackages: ["nodejs"],
|
|
45317
|
+
cloudVerifyCmd: "node --version",
|
|
45318
|
+
internal: true,
|
|
45319
|
+
preinstallJarvis: true,
|
|
45320
|
+
description: "Run Node.js scripts."
|
|
45321
|
+
},
|
|
45322
|
+
python3: {
|
|
45323
|
+
commodity: true,
|
|
45324
|
+
category: "development",
|
|
45325
|
+
pkg: "python3",
|
|
45326
|
+
installer: "system",
|
|
45327
|
+
label: "Python",
|
|
45328
|
+
summary: "Run Python code.",
|
|
45329
|
+
versionCmd: "{ python3 --version || python --version || py --version; } 2>/dev/null | sed -n 1p",
|
|
45330
|
+
preinstallCloud: true,
|
|
45331
|
+
cloudAptPackages: ["python3"],
|
|
45332
|
+
cloudVerifyCmd: "python3 --version",
|
|
45333
|
+
internal: true,
|
|
45334
|
+
preinstallJarvis: true,
|
|
45335
|
+
description: "Run Python scripts."
|
|
45336
|
+
},
|
|
45337
|
+
npm: {
|
|
45338
|
+
commodity: true,
|
|
45339
|
+
category: "development",
|
|
45340
|
+
pkg: "npm",
|
|
45341
|
+
installer: "system",
|
|
45342
|
+
label: "Node Package Manager",
|
|
45343
|
+
summary: "Install JavaScript packages.",
|
|
45344
|
+
versionCmd: "npm --version 2>/dev/null",
|
|
45345
|
+
preinstallCloud: true,
|
|
45346
|
+
cloudVerifyCmd: "npm --version",
|
|
45347
|
+
internal: true,
|
|
45348
|
+
description: "Install Node packages and run package scripts."
|
|
45349
|
+
},
|
|
45350
|
+
npx: {
|
|
45351
|
+
commodity: true,
|
|
45352
|
+
category: "development",
|
|
45353
|
+
pkg: "npx",
|
|
45354
|
+
installer: "system",
|
|
45355
|
+
label: "Node Package Runner",
|
|
45356
|
+
summary: "Run a JavaScript package without installing it.",
|
|
45357
|
+
versionCmd: "npx --version 2>/dev/null",
|
|
45358
|
+
preinstallCloud: true,
|
|
45359
|
+
cloudVerifyCmd: "npx --version",
|
|
45360
|
+
internal: true,
|
|
45361
|
+
description: "Run npm package binaries."
|
|
45362
|
+
},
|
|
45363
|
+
pip3: {
|
|
45364
|
+
preinstallCloud: true,
|
|
45365
|
+
cloudAptPackages: ["python3-pip"],
|
|
45366
|
+
cloudVerifyCmd: "pip3 --version",
|
|
45367
|
+
commodity: true,
|
|
45368
|
+
category: "development",
|
|
45369
|
+
pkg: "pip3",
|
|
45370
|
+
installer: "system",
|
|
45371
|
+
label: "Python Package Installer",
|
|
45372
|
+
summary: "Install Python packages.",
|
|
45373
|
+
versionCmd: "pip3 --version 2>/dev/null | sed -n 1p | cut -d' ' -f1-2",
|
|
45374
|
+
internal: true,
|
|
45375
|
+
description: "Install Python packages."
|
|
45376
|
+
},
|
|
45377
|
+
uv: {
|
|
45378
|
+
commodity: true,
|
|
45379
|
+
category: "development",
|
|
45380
|
+
pkg: "uv",
|
|
45381
|
+
installer: "system",
|
|
45382
|
+
label: "Python Package Manager",
|
|
45383
|
+
summary: "Install Python packages and manage environments, fast.",
|
|
45384
|
+
versionCmd: "uv --version 2>/dev/null",
|
|
45385
|
+
preinstallCloud: true,
|
|
45386
|
+
cloudInstallCmd: "curl -LsSf https://astral.sh/uv/0.9.6/install.sh | env UV_INSTALL_DIR=/usr/local/bin sh",
|
|
45387
|
+
cloudVerifyCmd: "uv --version",
|
|
45388
|
+
internal: true,
|
|
45389
|
+
description: "Manage Python dependencies, environments, tools, and scripts (`uv`, `uvx`)."
|
|
45390
|
+
},
|
|
45391
|
+
uvx: {
|
|
45392
|
+
commodity: true,
|
|
45393
|
+
category: "development",
|
|
45394
|
+
pkg: "uvx",
|
|
45395
|
+
installer: "system",
|
|
45396
|
+
label: "Python Tool Runner",
|
|
45397
|
+
summary: "Run a Python tool without installing it.",
|
|
45398
|
+
versionCmd: "uvx --version 2>/dev/null",
|
|
45399
|
+
preinstallCloud: true,
|
|
45400
|
+
cloudVerifyCmd: "uvx --version",
|
|
45401
|
+
internal: true,
|
|
45402
|
+
description: "Run Python tools in isolated environments."
|
|
45403
|
+
},
|
|
45404
|
+
ripgrep: {
|
|
45405
|
+
commodity: true,
|
|
45406
|
+
category: "development",
|
|
45407
|
+
pkg: "ripgrep",
|
|
45408
|
+
installer: "binary",
|
|
45409
|
+
label: "Search",
|
|
45410
|
+
summary: "Search text across files, very fast.",
|
|
45411
|
+
description: "Fast recursive grep with regex, .gitignore-aware, parallel, unicode-correct",
|
|
45412
|
+
versionCmd: "rg --version 2>/dev/null | sed -n 1p",
|
|
45413
|
+
preinstallCloud: true,
|
|
45414
|
+
binary: {
|
|
45415
|
+
"linux-x86_64": {
|
|
45416
|
+
url: "https://github.com/BurntSushi/ripgrep/releases/download/14.1.1/ripgrep-14.1.1-x86_64-unknown-linux-musl.tar.gz",
|
|
45417
|
+
archive: "tar.gz",
|
|
45418
|
+
extract: "ripgrep-14.1.1-x86_64-unknown-linux-musl/rg"
|
|
45419
|
+
},
|
|
45420
|
+
"linux-aarch64": {
|
|
45421
|
+
url: "https://github.com/BurntSushi/ripgrep/releases/download/14.1.1/ripgrep-14.1.1-aarch64-unknown-linux-gnu.tar.gz",
|
|
45422
|
+
archive: "tar.gz",
|
|
45423
|
+
extract: "ripgrep-14.1.1-aarch64-unknown-linux-gnu/rg"
|
|
45424
|
+
},
|
|
45425
|
+
"darwin-x86_64": {
|
|
45426
|
+
url: "https://github.com/BurntSushi/ripgrep/releases/download/14.1.1/ripgrep-14.1.1-x86_64-apple-darwin.tar.gz",
|
|
45427
|
+
archive: "tar.gz",
|
|
45428
|
+
extract: "ripgrep-14.1.1-x86_64-apple-darwin/rg"
|
|
45429
|
+
},
|
|
45430
|
+
"darwin-aarch64": {
|
|
45431
|
+
url: "https://github.com/BurntSushi/ripgrep/releases/download/14.1.1/ripgrep-14.1.1-aarch64-apple-darwin.tar.gz",
|
|
45432
|
+
archive: "tar.gz",
|
|
45433
|
+
extract: "ripgrep-14.1.1-aarch64-apple-darwin/rg"
|
|
45434
|
+
},
|
|
45435
|
+
"windows-x86_64": {
|
|
45436
|
+
url: "https://github.com/BurntSushi/ripgrep/releases/download/14.1.1/ripgrep-14.1.1-x86_64-pc-windows-msvc.zip",
|
|
45437
|
+
archive: "zip",
|
|
45438
|
+
extract: "ripgrep-14.1.1-x86_64-pc-windows-msvc/rg.exe"
|
|
45439
|
+
}
|
|
45440
|
+
},
|
|
45441
|
+
binName: "rg",
|
|
45442
|
+
internal: true,
|
|
45443
|
+
preinstallJarvis: true
|
|
45444
|
+
},
|
|
45445
|
+
"ast-grep": {
|
|
45446
|
+
category: "development",
|
|
45447
|
+
pkg: "ast-grep",
|
|
45448
|
+
installer: "binary",
|
|
45449
|
+
label: "Structural Search",
|
|
45450
|
+
summary: "Search and rewrite code by its structure, not just text.",
|
|
45451
|
+
description: 'Use when regex cannot distinguish syntax from text: `ast-grep -p "foo($$$)" -l ts` finds real calls while ignoring strings and comments; add `--rewrite "..."` for codemods. Prefer `rg` for literal or token search.',
|
|
45452
|
+
versionCmd: "ast-grep --version 2>/dev/null | sed -n 1p",
|
|
45453
|
+
preinstallCloud: true,
|
|
45454
|
+
binary: {
|
|
45455
|
+
"linux-x86_64": {
|
|
45456
|
+
url: "https://github.com/ast-grep/ast-grep/releases/download/0.44.1/app-x86_64-unknown-linux-gnu.zip",
|
|
45457
|
+
archive: "zip",
|
|
45458
|
+
extract: "ast-grep"
|
|
45459
|
+
},
|
|
45460
|
+
"linux-aarch64": {
|
|
45461
|
+
url: "https://github.com/ast-grep/ast-grep/releases/download/0.44.1/app-aarch64-unknown-linux-gnu.zip",
|
|
45462
|
+
archive: "zip",
|
|
45463
|
+
extract: "ast-grep"
|
|
45464
|
+
},
|
|
45465
|
+
"darwin-x86_64": {
|
|
45466
|
+
url: "https://github.com/ast-grep/ast-grep/releases/download/0.44.1/app-x86_64-apple-darwin.zip",
|
|
45467
|
+
archive: "zip",
|
|
45468
|
+
extract: "ast-grep"
|
|
45469
|
+
},
|
|
45470
|
+
"darwin-aarch64": {
|
|
45471
|
+
url: "https://github.com/ast-grep/ast-grep/releases/download/0.44.1/app-aarch64-apple-darwin.zip",
|
|
45472
|
+
archive: "zip",
|
|
45473
|
+
extract: "ast-grep"
|
|
45474
|
+
},
|
|
45475
|
+
"windows-x86_64": {
|
|
45476
|
+
url: "https://github.com/ast-grep/ast-grep/releases/download/0.44.1/app-x86_64-pc-windows-msvc.zip",
|
|
45477
|
+
archive: "zip",
|
|
45478
|
+
extract: "ast-grep.exe"
|
|
45479
|
+
}
|
|
45480
|
+
},
|
|
45481
|
+
binName: "ast-grep",
|
|
45482
|
+
internal: true,
|
|
45483
|
+
preinstallJarvis: true
|
|
45484
|
+
},
|
|
45485
|
+
fd: {
|
|
45486
|
+
commodity: true,
|
|
45487
|
+
category: "development",
|
|
45488
|
+
pkg: "fd-find",
|
|
45489
|
+
installer: "binary",
|
|
45490
|
+
label: "Find Files",
|
|
45491
|
+
summary: "Find files and folders by name.",
|
|
45492
|
+
description: "Fast user-friendly find replacement: regex by default, .gitignore-aware, parallel",
|
|
45493
|
+
versionCmd: "fd --version 2>/dev/null | sed -n 1p",
|
|
45494
|
+
preinstallCloud: true,
|
|
45495
|
+
binary: {
|
|
45496
|
+
"linux-x86_64": {
|
|
45497
|
+
url: "https://github.com/sharkdp/fd/releases/download/v10.2.0/fd-v10.2.0-x86_64-unknown-linux-musl.tar.gz",
|
|
45498
|
+
archive: "tar.gz",
|
|
45499
|
+
extract: "fd-v10.2.0-x86_64-unknown-linux-musl/fd"
|
|
45500
|
+
},
|
|
45501
|
+
"linux-aarch64": {
|
|
45502
|
+
url: "https://github.com/sharkdp/fd/releases/download/v10.2.0/fd-v10.2.0-aarch64-unknown-linux-gnu.tar.gz",
|
|
45503
|
+
archive: "tar.gz",
|
|
45504
|
+
extract: "fd-v10.2.0-aarch64-unknown-linux-gnu/fd"
|
|
45505
|
+
},
|
|
45506
|
+
"darwin-x86_64": {
|
|
45507
|
+
url: "https://github.com/sharkdp/fd/releases/download/v10.2.0/fd-v10.2.0-x86_64-apple-darwin.tar.gz",
|
|
45508
|
+
archive: "tar.gz",
|
|
45509
|
+
extract: "fd-v10.2.0-x86_64-apple-darwin/fd"
|
|
45510
|
+
},
|
|
45511
|
+
"darwin-aarch64": {
|
|
45512
|
+
url: "https://github.com/sharkdp/fd/releases/download/v10.2.0/fd-v10.2.0-aarch64-apple-darwin.tar.gz",
|
|
45513
|
+
archive: "tar.gz",
|
|
45514
|
+
extract: "fd-v10.2.0-aarch64-apple-darwin/fd"
|
|
45515
|
+
},
|
|
45516
|
+
"windows-x86_64": {
|
|
45517
|
+
url: "https://github.com/sharkdp/fd/releases/download/v10.2.0/fd-v10.2.0-x86_64-pc-windows-msvc.zip",
|
|
45518
|
+
archive: "zip",
|
|
45519
|
+
extract: "fd-v10.2.0-x86_64-pc-windows-msvc/fd.exe"
|
|
45520
|
+
}
|
|
45521
|
+
},
|
|
45522
|
+
binName: "fd",
|
|
45523
|
+
internal: true,
|
|
45524
|
+
preinstallJarvis: true
|
|
45525
|
+
},
|
|
45526
|
+
jq: {
|
|
45527
|
+
commodity: true,
|
|
45528
|
+
category: "development",
|
|
45529
|
+
pkg: "jq",
|
|
45530
|
+
installer: "binary",
|
|
45531
|
+
label: "JSON",
|
|
45532
|
+
summary: "Read and reshape JSON data.",
|
|
45533
|
+
description: "Command-line JSON processor: query, filter, transform, format JSON streams",
|
|
45534
|
+
versionCmd: "jq --version 2>/dev/null | sed -n 1p",
|
|
45535
|
+
preinstallCloud: true,
|
|
45536
|
+
binary: {
|
|
45537
|
+
"linux-x86_64": {
|
|
45538
|
+
url: "https://github.com/jqlang/jq/releases/download/jq-1.7.1/jq-linux-amd64",
|
|
45539
|
+
archive: "raw"
|
|
45540
|
+
},
|
|
45541
|
+
"linux-aarch64": {
|
|
45542
|
+
url: "https://github.com/jqlang/jq/releases/download/jq-1.7.1/jq-linux-arm64",
|
|
45543
|
+
archive: "raw"
|
|
45544
|
+
},
|
|
45545
|
+
"darwin-x86_64": {
|
|
45546
|
+
url: "https://github.com/jqlang/jq/releases/download/jq-1.7.1/jq-macos-amd64",
|
|
45547
|
+
archive: "raw"
|
|
45548
|
+
},
|
|
45549
|
+
"darwin-aarch64": {
|
|
45550
|
+
url: "https://github.com/jqlang/jq/releases/download/jq-1.7.1/jq-macos-arm64",
|
|
45551
|
+
archive: "raw"
|
|
45552
|
+
},
|
|
45553
|
+
"windows-x86_64": {
|
|
45554
|
+
url: "https://github.com/jqlang/jq/releases/download/jq-1.7.1/jq-windows-amd64.exe",
|
|
45555
|
+
archive: "raw"
|
|
45556
|
+
}
|
|
45557
|
+
},
|
|
45558
|
+
binName: "jq",
|
|
45559
|
+
internal: true,
|
|
45560
|
+
preinstallJarvis: true
|
|
45561
|
+
},
|
|
45562
|
+
patch: {
|
|
45563
|
+
commodity: true,
|
|
45564
|
+
category: "development",
|
|
45565
|
+
pkg: "patch",
|
|
45566
|
+
installer: "system",
|
|
45567
|
+
preinstallCloud: true,
|
|
45568
|
+
label: "Apply Diff",
|
|
45569
|
+
summary: "Apply a diff to files.",
|
|
45570
|
+
description: "Apply unified/context diff patches to files; reverse, dry-run, fuzzy matching",
|
|
45571
|
+
versionCmd: "patch --version 2>/dev/null | sed -n 1p",
|
|
45572
|
+
binName: "patch",
|
|
45573
|
+
internal: true,
|
|
45574
|
+
preinstallJarvis: true
|
|
45575
|
+
},
|
|
45576
|
+
sed: {
|
|
45577
|
+
commodity: true,
|
|
45578
|
+
category: "development",
|
|
45579
|
+
pkg: "sed",
|
|
45580
|
+
installer: "system",
|
|
45581
|
+
preinstallCloud: true,
|
|
45582
|
+
label: "Replace Text",
|
|
45583
|
+
summary: "Find and replace text in files.",
|
|
45584
|
+
description: "Stream editor: in-place text substitution, line filtering, scripted edits",
|
|
45585
|
+
versionCmd: "sed --version 2>/dev/null | sed -n 1p",
|
|
45586
|
+
binName: "sed",
|
|
45587
|
+
internal: true,
|
|
45588
|
+
preinstallJarvis: true
|
|
45589
|
+
},
|
|
45590
|
+
grep: {
|
|
45591
|
+
commodity: true,
|
|
45592
|
+
category: "development",
|
|
45593
|
+
pkg: "grep",
|
|
45594
|
+
installer: "system",
|
|
45595
|
+
preinstallCloud: true,
|
|
45596
|
+
label: "Search Text",
|
|
45597
|
+
summary: "Search for text in files.",
|
|
45598
|
+
description: "Search plain text for lines matching a regex: recursive, fixed-string, context, invert, count",
|
|
45599
|
+
versionCmd: "grep --version 2>/dev/null | sed -n 1p",
|
|
45600
|
+
binName: "grep",
|
|
45601
|
+
internal: true,
|
|
45602
|
+
preinstallJarvis: true
|
|
45603
|
+
},
|
|
45604
|
+
"wordpress-api": {
|
|
45605
|
+
preinstallCloud: true,
|
|
45606
|
+
category: "development",
|
|
45607
|
+
pkg: "@todoforai/wordpress-api",
|
|
45608
|
+
installer: "npm",
|
|
45609
|
+
label: "WordPress",
|
|
45610
|
+
summary: "Read and publish WordPress posts and pages.",
|
|
45611
|
+
statusCmd: `wordpress-api whoami 2>/dev/null | sed -n 's/.*"slug"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/p' | grep -m10 .`,
|
|
45612
|
+
loginCmd: "wordpress-api auth --site <url> --user <username> --password <app_password>",
|
|
45613
|
+
credentialPaths: ["~/.config/wordpress-api/credentials.json"],
|
|
45614
|
+
description: "WordPress REST API CLI. `wordpress-api auth --site https://example.com --user <user> --password '<application password>'` (application password, not the login password). Reading published content needs no auth. `posts list|get|create|update|delete`, `pages`, `media`, `categories`. `posts create` saves a draft unless --status publish. On Elementor/JetEngine sites content.rendered comes back empty — fetch the post's public URL for the body.",
|
|
45615
|
+
versionCmd: "wordpress-api --version 2>/dev/null | sed -n 1p"
|
|
45616
|
+
},
|
|
45617
|
+
"facebook-pages-api": {
|
|
45618
|
+
preinstallCloud: true,
|
|
45619
|
+
category: "social",
|
|
45620
|
+
pkg: "@todoforai/facebook-pages-api",
|
|
45621
|
+
installer: "npm",
|
|
45622
|
+
label: "Facebook Pages",
|
|
45623
|
+
summary: "Post to your Facebook Page and handle comments.",
|
|
45624
|
+
statusCmd: `facebook-pages-api whoami 2>/dev/null | sed -n 's/.*"name"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/p' | grep -m10 .`,
|
|
45625
|
+
loginCmd: "facebook-pages-api auth --page-id <id> --page-token <token>",
|
|
45626
|
+
credentialPaths: ["~/.config/facebook-pages-api/credentials.json"],
|
|
45627
|
+
description: "Facebook Pages CLI (Meta Graph API). `auth --page-id <id> --page-token <token>` (or env FB_PAGE_ID/FB_PAGE_TOKEN). `feed`, `comments`, `insights`, `conversations`, `media`, and `request <GET|POST|DELETE> <path> [--data <json>]` = any Graph edge with the page token attached (use instead of curl). Writes are live: `feed create --published false` for tests; `comments reply` posts publicly — get human approval. Follower totals: `whoami --fields followers_count,fan_count` (many classic insights metrics are retired). `--help` for subcommands.",
|
|
45628
|
+
versionCmd: "facebook-pages-api --version 2>/dev/null | sed -n 1p"
|
|
45629
|
+
},
|
|
45630
|
+
"ews-api": {
|
|
45631
|
+
category: "email",
|
|
45632
|
+
pkg: "ews-api",
|
|
45633
|
+
installer: "pip",
|
|
45634
|
+
label: "Exchange (EWS)",
|
|
45635
|
+
summary: "Read and write mail on a company Exchange server.",
|
|
45636
|
+
statusCmd: `ews-api whoami 2>/dev/null | sed -n 's/.*"email"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/p' | grep -m10 .`,
|
|
45637
|
+
loginCmd: "ews-api auth --email <email> --username <username> --server <server> --password <password>",
|
|
45638
|
+
credentialPaths: ["~/.config/ews-api/credentials.json"],
|
|
45639
|
+
description: "On-prem Microsoft Exchange mail over EWS (cloud/M365 → outlook-api). `auth --email <e> --username 'DOMAIN\\\\user' --server <host>` (NTLM; password prompt or `--password-stdin`; --server required). Multi-mailbox: `whoami`, `--account <email>`/EWS_ACCOUNT, `default`, `logout`; EWS_EMAIL/EWS_USERNAME/EWS_PASSWORD/EWS_SERVER for unattended runs. `list`, `search <q>`, `read <id>`, `draft`, `reply <id>` — draft/reply stay in Drafts unless --send. Password stored plaintext in ~/.config/ews-api (0600); re-auth after rotation.",
|
|
45640
|
+
versionCmd: "ews-api --version 2>/dev/null | sed -n 1p",
|
|
45641
|
+
logoutCmd: "ews-api logout {{ACCOUNT}}"
|
|
45642
|
+
},
|
|
45643
|
+
"outlook-api": {
|
|
45644
|
+
preinstallCloud: true,
|
|
45645
|
+
category: "email",
|
|
45646
|
+
pkg: "@todoforai/outlook-api",
|
|
45647
|
+
installer: "npm",
|
|
45648
|
+
label: "Outlook (Graph)",
|
|
45649
|
+
summary: "Read and write your Outlook mail.",
|
|
45650
|
+
statusCmd: `outlook-api whoami 2>/dev/null | sed -n 's/.*"userPrincipalName"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/p' | grep -m10 .`,
|
|
45651
|
+
loginCmd: "outlook-api auth",
|
|
45652
|
+
credentialPaths: ["~/.config/outlook-api/config.json"],
|
|
45653
|
+
description: "Outlook/Hotmail/M365 mail over Microsoft Graph (on-prem Exchange → ews-api). `auth` = device-code flow; `--tenant consumers` (default, personal) | `organizations`. list/search/read/attachments are read-only; `draft`/`reply` stay in Drafts unless --send; `send <draftId>`. `get <path>` = raw Graph GET. JSON out.",
|
|
45654
|
+
versionCmd: "outlook-api --version 2>/dev/null | sed -n 1p",
|
|
45655
|
+
deviceCodePattern: "enter the code ([A-Z0-9]+)",
|
|
45656
|
+
oauthUrlPattern: "(https://(?:www\\.microsoft\\.com/link|login\\.microsoft\\.com/device))",
|
|
45657
|
+
canStreamLogin: true
|
|
45658
|
+
},
|
|
45659
|
+
neonctl: {
|
|
45660
|
+
category: "database",
|
|
45661
|
+
pkg: "neonctl",
|
|
45662
|
+
installer: "npm",
|
|
45663
|
+
label: "Neon Postgres",
|
|
45664
|
+
summary: "Manage your Neon Postgres databases and branches.",
|
|
45665
|
+
statusCmd: `test -f ~/.config/neon/credentials.json && neonctl me --output json 2>/dev/null | sed -n 's/.*"email"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/p' | grep -m10 .`,
|
|
45666
|
+
loginCmd: "neonctl auth",
|
|
45667
|
+
oauthUrlPattern: "(https://oauth2\\.neon\\.tech/oauth2/auth\\S+)",
|
|
45668
|
+
credentialPaths: ["~/.config/neon/credentials.json"],
|
|
45669
|
+
description: "Manage Neon Postgres branches and run SQL: `neonctl projects list`, `neonctl branches create`, `neonctl connection-string`. Aliased as `neon`.",
|
|
45670
|
+
versionCmd: "neonctl --version 2>/dev/null | sed -n 1p",
|
|
45671
|
+
aliases: ["neon"],
|
|
45672
|
+
canStreamLogin: true
|
|
45673
|
+
},
|
|
45674
|
+
m365: {
|
|
45675
|
+
preinstallCloud: true,
|
|
45676
|
+
category: "productivity",
|
|
45677
|
+
pkg: "@pnp/cli-microsoft365",
|
|
45678
|
+
installer: "npm",
|
|
45679
|
+
label: "Microsoft 365",
|
|
45680
|
+
summary: "Work with Microsoft 365 mail, Teams, SharePoint and users.",
|
|
45681
|
+
statusCmd: `m365 status --output text 2>/dev/null | tr -d '"' | grep -vi 'logged out' | grep -m1 .`,
|
|
45682
|
+
loginCmd: "m365 login --authType deviceCode --appId 04b07795-8ddb-461a-bbee-02f9e1bf7b46",
|
|
45683
|
+
deviceCodePattern: "enter the code ([A-Z0-9]{8,}) to authenticate",
|
|
45684
|
+
oauthUrlPattern: "open the page (https://\\S+) and enter",
|
|
45685
|
+
credentialPaths: ["~/.cli-m365-msal.json", "~/.cli-m365-connection.json"],
|
|
45686
|
+
description: "Microsoft 365 from the shell: `m365 outlook message list`, `m365 teams team list`, `m365 spo site list`. Device-code login; add `--output json` for parseable output.",
|
|
45687
|
+
versionCmd: `m365 version 2>/dev/null | tr -d '"' | sed -n 1p`
|
|
45688
|
+
},
|
|
45689
|
+
hubspot: {
|
|
45690
|
+
preinstallCloud: true,
|
|
45691
|
+
category: "marketing",
|
|
45692
|
+
pkg: "@hubspot/cli",
|
|
45693
|
+
installer: "npm",
|
|
45694
|
+
label: "HubSpot",
|
|
45695
|
+
summary: "Work with your HubSpot contacts, deals and content.",
|
|
45696
|
+
binName: "hs",
|
|
45697
|
+
statusCmd: "hs account list >/dev/null 2>&1 && echo authenticated",
|
|
45698
|
+
loginCmd: "hs account auth",
|
|
45699
|
+
oauthUrlPattern: "(https://app\\.hubspot\\.com/l/personal-access-key\\S+)",
|
|
45700
|
+
loginNeedsPty: true,
|
|
45701
|
+
credentialPaths: ["~/.hscli/config.yml"],
|
|
45702
|
+
description: "Invoked as `hs`. `hs api <endpoint>` makes authenticated calls to any HubSpot API (contacts, deals, companies) — prefer it over raw curl.",
|
|
45703
|
+
versionCmd: "hs --version 2>/dev/null | sed -n 1p",
|
|
45704
|
+
canStreamLogin: true
|
|
45705
|
+
},
|
|
45706
|
+
"posthog-cli": {
|
|
45707
|
+
preinstallCloud: true,
|
|
45708
|
+
category: "monitoring",
|
|
45709
|
+
pkg: "@posthog/cli",
|
|
45710
|
+
installer: "npm",
|
|
45711
|
+
label: "PostHog",
|
|
45712
|
+
summary: "Query PostHog product analytics and manage feature flags.",
|
|
45713
|
+
statusCmd: `grep -q '"token"' "\${POSTHOG_HOME:-$HOME/.posthog}/credentials.json" 2>/dev/null && echo authenticated`,
|
|
45714
|
+
loginCmd: "posthog-cli login",
|
|
45715
|
+
loginTerminalHint: "Interactive only: run `posthog-cli login` in a terminal on this host (it asks for host, project and a personal API key), or set `POSTHOG_CLI_API_KEY` + `POSTHOG_CLI_PROJECT_ID`.",
|
|
45716
|
+
credentialPaths: ["~/.posthog/credentials.json"],
|
|
45717
|
+
description: "Official PostHog CLI. `posthog-cli api tools` lists the agent-first API tool catalog, `posthog-cli api call <tool> '<json>'` runs one (SQL queries, insights, flags). Also uploads sourcemaps/dSYM. Env alternative: POSTHOG_CLI_API_KEY + POSTHOG_CLI_PROJECT_ID.",
|
|
45718
|
+
versionCmd: "posthog-cli --version 2>/dev/null | sed -n 1p"
|
|
45719
|
+
},
|
|
45720
|
+
canva: {
|
|
45721
|
+
category: "design",
|
|
45722
|
+
pkg: "@canva/cli",
|
|
45723
|
+
installer: "npm",
|
|
45724
|
+
label: "Canva",
|
|
45725
|
+
summary: "Build and deploy Canva apps.",
|
|
45726
|
+
statusCmd: "test -s ~/.canva-cli/credentials && echo authenticated",
|
|
45727
|
+
loginCmd: "canva login --mode manual --lite",
|
|
45728
|
+
loginNeedsPty: true,
|
|
45729
|
+
stdinPaste: true,
|
|
45730
|
+
credentialPaths: ["~/.canva-cli/credentials"],
|
|
45731
|
+
description: "Official Canva CLI — app-development focused: `canva apps create/list/preview`. `canva mcp` starts the Canva Developer MCP server. For design/export automation use the Canva Connect REST API.",
|
|
45732
|
+
versionCmd: "canva --version 2>/dev/null | sed -n 1p",
|
|
45733
|
+
oauthUrlPattern: "OPEN_URL: (https://www\\.canva\\.com/api/oauth/authorize\\S+)"
|
|
45734
|
+
},
|
|
45735
|
+
webflow: {
|
|
45736
|
+
category: "development",
|
|
45737
|
+
pkg: "@webflow/webflow-cli",
|
|
45738
|
+
installer: "npm",
|
|
45739
|
+
label: "Webflow",
|
|
45740
|
+
summary: "Manage Webflow sites, CMS content and assets.",
|
|
45741
|
+
statusCmd: "webflow auth status --skip-update-check >/dev/null 2>&1 && echo authenticated",
|
|
45742
|
+
loginCmd: "webflow auth login --skip-update-check",
|
|
45743
|
+
loginNeedsPty: true,
|
|
45744
|
+
oauthUrlPattern: "(https://webflow\\.com/oauth/webflow/initiate\\S+)",
|
|
45745
|
+
credentialPaths: ["~/.config/webflow"],
|
|
45746
|
+
description: "Manage Webflow sites from the shell: `webflow sites list`, `webflow cms ...`, `webflow assets ...`. Add `--skip-update-check` in scripts.",
|
|
45747
|
+
versionCmd: "webflow --version 2>/dev/null | sed -n 1p"
|
|
45748
|
+
},
|
|
45749
|
+
readwise: {
|
|
45750
|
+
category: "productivity",
|
|
45751
|
+
pkg: "@readwise/cli",
|
|
45752
|
+
installer: "npm",
|
|
45753
|
+
label: "Readwise",
|
|
45754
|
+
summary: "Search and save articles and highlights in Readwise.",
|
|
45755
|
+
statusCmd: "readwise whoami >/dev/null 2>&1 && readwise whoami 2>/dev/null | sed 's/\\x1b\\[[0-9;]*m//g' | grep -m1 .",
|
|
45756
|
+
loginCmd: "readwise login",
|
|
45757
|
+
credentialPaths: ["~/.readwise-cli.json"],
|
|
45758
|
+
description: "Official Readwise/Reader CLI. `readwise search <query>`, `readwise list`, `readwise save <url>`; supports JSON output for piping and a read-only mode for agents. Token login for CI: `readwise login-with-token <token>` (get one at readwise.io/access_token).",
|
|
45759
|
+
versionCmd: "readwise --version 2>/dev/null | sed -n 1p",
|
|
45760
|
+
oauthUrlPattern: "OPEN_URL: (https://readwise\\.io/o/authorize\\S+)",
|
|
45761
|
+
oauthCallbackPort: 6274
|
|
45762
|
+
},
|
|
45763
|
+
granola: {
|
|
45764
|
+
preinstallCloud: true,
|
|
45765
|
+
category: "productivity",
|
|
45766
|
+
pkg: "granola-cli",
|
|
45767
|
+
installer: "npm",
|
|
45768
|
+
binName: "granola",
|
|
45769
|
+
label: "Granola",
|
|
45770
|
+
summary: "Read your Granola meeting notes and transcripts.",
|
|
45771
|
+
statusCmd: `granola auth status -o json 2>/dev/null | sed -n 's/.*"authenticated": *true.*/authenticated/p' | grep -m1 .`,
|
|
45772
|
+
loginCmd: "granola auth login",
|
|
45773
|
+
loginTerminalHint: "Imports the Granola desktop app's session: install Granola on this host, sign in there, then run `granola auth login`.",
|
|
45774
|
+
credentialPaths: ["~/.config/keyring", "~/.config/granola"],
|
|
45775
|
+
versionCmd: "granola --version 2>/dev/null | sed -n 1p",
|
|
45776
|
+
description: "Community Granola CLI (read-only). `granola meeting list [-l N] [--since <date>] [-s <query>] [-a <attendee>] -o json`, `granola meeting notes|enhanced|transcript <id>`, `granola meeting export <id>`; `granola folder list`, `granola workspace list`. `-o json|yaml|toon` for piping. Login imports credentials from the installed Granola desktop app (`granola auth login`) — desktop app must be logged in on the same machine; transcripts need a paid Granola plan."
|
|
45777
|
+
},
|
|
45778
|
+
trello: {
|
|
45779
|
+
preinstallCloud: true,
|
|
45780
|
+
category: "productivity",
|
|
45781
|
+
pkg: "trello-cli",
|
|
45782
|
+
installer: "npm",
|
|
45783
|
+
binName: "trello",
|
|
45784
|
+
label: "Trello",
|
|
45785
|
+
summary: "Manage your Trello boards, lists and cards.",
|
|
45786
|
+
statusCmd: "trello board:list --format json </dev/null >/dev/null 2>&1 && echo authenticated",
|
|
45787
|
+
loginCmd: "trello auth:api-key <key> && trello auth:token <token>",
|
|
45788
|
+
credentialPaths: [
|
|
45789
|
+
"~/.trello-cli"
|
|
45790
|
+
],
|
|
45791
|
+
versionCmd: "trello --version 2>/dev/null | sed -n 1p",
|
|
45792
|
+
description: "Community Trello CLI (mheap/trello-cli, no official one exists). Auth is a Trello API key + token from https://trello.com/app-key — `trello auth:api-key <key>` then `trello auth:token <token>`. `board:list`, `list:list --board <name>`, `card:list --board <b> --list <l>`, `card:create --board <b> --list <l> --name <n> [--description ..] [--due <date>]`, `card:move`, `card:comment`, `label:*`, `search <q>`. Add `--format json` for piping. Names resolve via a local cache; run `trello board:list` once after login to warm it."
|
|
45793
|
+
},
|
|
45794
|
+
mapps: {
|
|
45795
|
+
category: "development",
|
|
45796
|
+
pkg: "@mondaycom/apps-cli",
|
|
45797
|
+
installer: "npm",
|
|
45798
|
+
label: "monday.com",
|
|
45799
|
+
summary: "Build and deploy monday.com apps.",
|
|
45800
|
+
statusCmd: "grep -qsE '[A-Za-z0-9]' ~/.config/mapps/.mappsrc ~/.mappsrc && echo authenticated",
|
|
45801
|
+
loginCmd: "mapps init -t <api_token>",
|
|
45802
|
+
credentialPaths: ["~/.config/mapps/.mappsrc", "~/.mappsrc"],
|
|
45803
|
+
description: "Invoked as `mapps` — app development and monday-code deployments only; `mapps init -t <token>` stores a monday.com API token in ~/.mappsrc. To read/write board DATA use the monday.com GraphQL API (api.monday.com/v2) with that same token.",
|
|
45804
|
+
versionCmd: "mapps --version 2>/dev/null | sed -n 1p"
|
|
45805
|
+
},
|
|
45806
|
+
"exa-cli": {
|
|
45807
|
+
preinstallCloud: true,
|
|
45808
|
+
category: "utility",
|
|
45809
|
+
pkg: "exa-cli",
|
|
45810
|
+
installer: "npm",
|
|
45811
|
+
label: "Exa Search",
|
|
45812
|
+
summary: "Search the web and pull page contents, built for AI.",
|
|
45813
|
+
binName: "exa",
|
|
45814
|
+
statusCmd: 'test -n "$EXA_API_KEY" && echo authenticated',
|
|
45815
|
+
credentialPaths: [],
|
|
45816
|
+
description: "Invoked as `exa`. `exa search <query>`, `exa answer <question>`, `exa contents <url>`, `exa find-similar <url>` — JSON by default. Needs EXA_API_KEY in the environment (get one at dashboard.exa.ai); store it in the vault.",
|
|
45817
|
+
versionCmd: "exa --version 2>/dev/null | sed -n 1p"
|
|
45818
|
+
},
|
|
45819
|
+
mobbin: {
|
|
45820
|
+
category: "design",
|
|
45821
|
+
pkg: "mobbin",
|
|
45822
|
+
installer: "npm",
|
|
45823
|
+
label: "Mobbin",
|
|
45824
|
+
summary: "Browse real app and website design examples.",
|
|
45825
|
+
statusCmd: "mobbin auth print-cookie 2>/dev/null | grep -E '[A-Za-z0-9]' >/dev/null && echo authenticated",
|
|
45826
|
+
loginCmd: "mobbin auth save-cookie",
|
|
45827
|
+
loginTerminalHint: "Log in to mobbin.com in a browser on this host, then run `mobbin auth save-cookie`: it imports that browser's cookies.",
|
|
45828
|
+
credentialPaths: ["~/.config/mobbin/cookie.txt", "~/.config/mobbin/storage-state.json"],
|
|
45829
|
+
description: 'Unofficial CLI over Mobbin\'s undocumented web endpoints. `mobbin search --query "login" --platform ios`. Auth reuses browser cookies: log into mobbin.com in Chrome, then `mobbin auth save-cookie`.',
|
|
45830
|
+
versionCmd: "mobbin --version 2>/dev/null | sed -n '1,2p' | tr '\\n' ' '"
|
|
45831
|
+
},
|
|
45832
|
+
"blender-mcp": {
|
|
45833
|
+
preinstallCloud: true,
|
|
45834
|
+
packages: ["blender-mcp==1.9.1"],
|
|
45835
|
+
category: "design",
|
|
45836
|
+
pkg: "blender-mcp",
|
|
45837
|
+
installer: "pip",
|
|
45838
|
+
label: "Blender",
|
|
45839
|
+
summary: "Model, texture and render 3D scenes in a running Blender.",
|
|
45840
|
+
statusCmd: `command -v blender-mcp >/dev/null && mcporter call blender.get_addon_status --output json 2>/dev/null | grep -o '"blender_version": "[^"]*"' | grep -m1 .`,
|
|
45841
|
+
loginNoBrowser: true,
|
|
45842
|
+
loginTerminalHint: "Needs Blender (blender.org) open on this host: in Blender, Edit → Preferences → Add-ons, enable `Blender MCP`, then press N in the 3D view → BlenderMCP tab → Connect / Start MCP server. Then press Connect here.",
|
|
45843
|
+
loginCmd: `blender-mcp install-addon >/dev/null 2>&1; command -v mcporter >/dev/null 2>&1 || npm install -g --prefix "$HOME/.local" mcporter; mcporter config add blender --command blender-mcp --env BLENDER_MCP_DISABLE_TELEMETRY=1 --description 'Blender 3D via ahujasid/blender-mcp addon' --scope home`,
|
|
45844
|
+
logoutCmd: "cd ~ && mcporter config remove blender",
|
|
45845
|
+
credentialPaths: ["~/.mcporter/mcporter.json"],
|
|
45846
|
+
description: "Blender 3D via the ahujasid/blender-mcp server through mcporter: `mcporter list blender --schema`, `mcporter call blender.<tool> key=value` (`code=@file.py`; every tool needs `user_prompt`). Workhorse: execute_blender_code (any bpy Python); also scene/object info, viewport screenshot (`--save-images <dir>`), PolyHaven/Sketchfab import. Needs a GUI Blender with the addon server started (socket :9876; headless has none). 'Could not connect' → `blender-mcp install-addon`, enable in Preferences → Add-ons, N-panel → Start MCP Server (scripted: `blender --python s.py`, s.py registers a `bpy.app.timers` calling `bpy.ops.blendermcp.start_server()`). Code runs unguarded: save first (`bpy.ops.wm.save_mainfile()`), approve destructive ops.",
|
|
45847
|
+
versionCmd: `{ python -c 'from importlib.metadata import version; print(version("blender-mcp"))' || python3 -c 'from importlib.metadata import version; print(version("blender-mcp"))'; } 2>/dev/null`
|
|
45848
|
+
},
|
|
45849
|
+
"google-ads": {
|
|
45850
|
+
category: "marketing",
|
|
45851
|
+
pkg: "google-ads",
|
|
45852
|
+
packages: [
|
|
45853
|
+
"google-ads==32.0.0"
|
|
45854
|
+
],
|
|
45855
|
+
installer: "pip",
|
|
45856
|
+
label: "Google Ads",
|
|
45857
|
+
summary: "Query and manage Google Ads campaigns, keywords and conversions.",
|
|
45858
|
+
loginCmd: `printf 'developer_token: %s\\nclient_id: %s\\nclient_secret: %s\\nrefresh_token: %s\\nlogin_customer_id: %s\\nuse_proto_plus: true\\n' <developer_token> <client_id> <client_secret> <refresh_token> <mcc_id_digits_only> > "$HOME/google-ads.yaml" && python3 -c 'from google.ads.googleads.client import GoogleAdsClient; GoogleAdsClient.load_from_storage(); print("ok")'`,
|
|
45859
|
+
credentialPaths: [
|
|
45860
|
+
"~/google-ads.yaml"
|
|
45861
|
+
],
|
|
45862
|
+
statusCmd: `python3 -c 'from google.ads.googleads.client import GoogleAdsClient; c=GoogleAdsClient.load_from_storage(); print(c.login_customer_id or "authenticated")' 2>/dev/null | grep -m1 .`,
|
|
45863
|
+
versionCmd: "python3 -c 'import google.ads.googleads as g; print(g.VERSION)' 2>/dev/null",
|
|
45864
|
+
cloudVerifyCmd: "python3 -c 'import google.ads.googleads'",
|
|
45865
|
+
description: "Official Google Ads API client library (Python) — there is no official CLI, so call it via `python3 -c` or a script. Config `~/google-ads.yaml` (developer_token, client_id/secret, refresh_token, login_customer_id = MCC digits). Mint refresh_token with the bundled `google-oauthlib-tool --client-secrets <json> --scope https://www.googleapis.com/auth/adwords --save --headless` (or examples/authentication/generate_user_credentials.py). Read: `client.get_service('GoogleAdsService').search_stream(customer_id=<digits>, query=<GAQL>)`. Write: `<X>Service.mutate_<x>s(customer_id, operations, validate_only=True)` first, then real. Keyword ideas: `KeywordPlanIdeaService.generate_keyword_ideas`. Offline conversions: `ConversionUploadService.upload_click_conversions`. Customer ids are digits without dashes."
|
|
45866
|
+
},
|
|
45867
|
+
"cua-driver": {
|
|
45868
|
+
category: "development",
|
|
45869
|
+
pkg: "cua-driver",
|
|
45870
|
+
installer: "system",
|
|
45871
|
+
label: "Desktop Control",
|
|
45872
|
+
summary: "Click, type and read native desktop apps without taking over the mouse or focus.",
|
|
45873
|
+
installCmd: 'curl -fsSL https://cua.ai/driver/install.sh | CUA_DRIVER_NO_MODIFY_PATH=1 CUA_DRIVER_BIN_DIR="$HOME/.todoforai/tools/bin" bash',
|
|
45874
|
+
uninstallCmd: 'curl -fsSL https://cua.ai/driver/uninstall.sh | bash; rm -f "$HOME/.todoforai/tools/bin/cua-driver" "$HOME/.todoforai/tools/bin/cua-cursor-theme"; hash -r 2>/dev/null; ! command -v cua-driver >/dev/null 2>&1 || { echo "cua-driver is still on PATH: $(command -v cua-driver)"; exit 1; }',
|
|
45875
|
+
installCmdWindows: "powershell.exe -NoProfile -ExecutionPolicy Bypass -Command '$ProgressPreference='\\''SilentlyContinue'\\''; $ErrorActionPreference='\\''Stop'\\''; [Net.ServicePointManager]::SecurityProtocol=[Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12; $t=Join-Path $env:USERPROFILE '\\''.todoforai\\tools'\\''; $d=Join-Path $t '\\''bin'\\''; New-Item -ItemType Directory -Force -Path $d | Out-Null; $env:CUA_DRIVER_RS_INSTALL_DIR=Join-Path $t '\\''cua-driver\\bin'\\''; $f=Join-Path $env:TEMP '\\''cua-driver-install.ps1'\\''; Invoke-RestMethod -UseBasicParsing https://cua.ai/driver/install.ps1 | Set-Content -LiteralPath $f -Encoding UTF8; & $f -NoAutoStart -NoPathUpdate; if($LASTEXITCODE){exit $LASTEXITCODE}; $u=New-Object Text.UTF8Encoding $false; [IO.File]::WriteAllText((Join-Path $d '\\''cua-driver'\\''), \"#!/bin/sh`nexec `\"`$(dirname `\"`$0`\")/../cua-driver/bin/cua-driver.exe`\" `\"`$@`\"`n\", $u); [IO.File]::WriteAllText((Join-Path $d '\\''cua-driver.cmd'\\''), \"@echo off`r`n`\"%~dp0..\\cua-driver\\bin\\cua-driver.exe`\" %*`r`n\", $u)'",
|
|
45876
|
+
uninstallCmdWindows: "powershell.exe -NoProfile -ExecutionPolicy Bypass -Command '$ErrorActionPreference='\\''Continue'\\''; $t=Join-Path $env:USERPROFILE '\\''.todoforai\\tools'\\''; $d=Join-Path $t '\\''bin'\\''; $j=Join-Path $t '\\''cua-driver\\bin'\\''; $exe=Join-Path $j '\\''cua-driver.exe'\\''; if(Test-Path -LiteralPath $exe){& $exe stop 2>$null | Out-Null}; Stop-Process -Name cua-driver -Force -ErrorAction SilentlyContinue; $env:CUA_DRIVER_RS_UNINSTALL_FORCE='\\''1'\\''; $env:CUA_DRIVER_RS_INSTALL_DIR=$j; Invoke-RestMethod -UseBasicParsing https://cua.ai/driver/uninstall.ps1 | Invoke-Expression; Remove-Item -Force -ErrorAction SilentlyContinue (Join-Path $d '\\''cua-driver'\\''), (Join-Path $d '\\''cua-driver.cmd'\\''); if(Test-Path -LiteralPath $j){[IO.Directory]::Delete($j,$false)}; Remove-Item -Recurse -Force -ErrorAction SilentlyContinue (Join-Path $t '\\''cua-driver'\\'')'",
|
|
45877
|
+
versionCmd: "cua-driver --version 2>/dev/null | sed -n 1p",
|
|
45878
|
+
description: 'Native desktop automation (macOS/Windows/Linux X11; Wayland only via AT-SPI, no window screenshots) through the accessibility tree — actions land in the BACKGROUND without moving the user\'s focus or pointer. Needs a daemon: `cua-driver status || (cua-driver serve --no-overlay >/dev/null 2>&1 &)`; on macOS grant permissions first with `cua-driver permissions grant`; on Windows the daemon must run in the interactive desktop session (`cua-driver autostart enable` registers a logon task). Loop: `cua-driver call list_windows \'{}\'` → pid+window_id; `cua-driver call get_window_state \'{"pid":P,"window_id":W,"include_screenshot":false}\'` → `elements[]` with `element_token`; act with `cua-driver call click \'{"pid":P,"window_id":W,"element_token":"s..:N"}\'`, `type_text {..,"text":".."}`, `hotkey {..,"keys":["ctrl","s"]}`, `press_key`, `scroll`, `invoke_menu`. Tokens are snapshot-bound: re-run get_window_state after EVERY action (stale token = refusal). Window screenshot: add `--screenshot-out-file out.png` (`max_dimension` to shrink). Responses say `effect:unverifiable` — verify from a fresh tree, not from the click reply. `delivery_mode:"foreground"` steals focus; use only when asked. Chrome/Electron trees are near-empty — use agent-browser for web pages. `cua-driver list-tools` / `describe <tool>` for the full surface; `cua-driver mcp` exposes the same over stdio MCP.'
|
|
45879
|
+
}
|
|
45880
|
+
};
|
|
45881
|
+
|
|
45882
|
+
// ../packages/shared-fbe/src/toolCatalog.ts
|
|
45883
|
+
var TOOL_CATALOG = tool_catalog_default;
|
|
45884
|
+
// ../packages/shared-fbe/src/toolInstallCommand.ts
|
|
45885
|
+
var KEY_OF = new Map(Object.entries(TOOL_CATALOG).map(([k, e]) => [e, k]));
|
|
45886
|
+
var NODE_MIN_MAJOR = 24;
|
|
45887
|
+
var NODE_OK = `command -v npm >/dev/null 2>&1 && node -e 'process.exit(+process.versions.node.split(".")[0]>=${NODE_MIN_MAJOR}?0:1)' >/dev/null 2>&1`;
|
|
44335
45888
|
// ../packages/shared-fbe/src/model_promos.json
|
|
44336
45889
|
var model_promos_default = {
|
|
44337
45890
|
_doc: [
|
|
@@ -44483,6 +46036,10 @@ var TOPICS = {
|
|
|
44483
46036
|
channel: (p) => SERVER_TO_FRONTENDS.todo.message_removed(p.todoId),
|
|
44484
46037
|
audience: "frontend"
|
|
44485
46038
|
},
|
|
46039
|
+
["todo:comment_update" /* TODO_COMMENT_UPDATE */]: {
|
|
46040
|
+
channel: (p) => SERVER_TO_FRONTENDS.todo.commentUpdate(p.todoId),
|
|
46041
|
+
audience: "frontend"
|
|
46042
|
+
},
|
|
44486
46043
|
["todo:msg_meta_usr" /* TODO_MSG_META_USR */]: {
|
|
44487
46044
|
channel: (p) => SERVER_TO_FRONTENDS.todo.msgMetaUsr(p.todoId),
|
|
44488
46045
|
audience: "frontend"
|
|
@@ -44559,7 +46116,7 @@ import { parseArgs } from "util";
|
|
|
44559
46116
|
// package.json
|
|
44560
46117
|
var package_default = {
|
|
44561
46118
|
name: "@todoforai/cli",
|
|
44562
|
-
version: "0.1.
|
|
46119
|
+
version: "0.1.56",
|
|
44563
46120
|
type: "module",
|
|
44564
46121
|
bin: {
|
|
44565
46122
|
"todoforai-cli": "bin/todoforai-cli.js",
|
|
@@ -44627,6 +46184,9 @@ Examples:
|
|
|
44627
46184
|
tfa-cli show list|rm … # List / take down shown files
|
|
44628
46185
|
tfa-cli open <url> [todo-id] # Live url preview in the chat
|
|
44629
46186
|
tfa-cli recommend --template <id> # Add a template as a recommendation card
|
|
46187
|
+
tfa-cli inbox [needs-you|messages|activity] [--unread] # Read the notification feed (the bell)
|
|
46188
|
+
tfa-cli inbox seen [tab] # Mark notifications read
|
|
46189
|
+
tfa-cli notify "Title" "message" [--href /t/<id>] # Note to YOUR OWN user only (Messages tab + phone push); no --to
|
|
44630
46190
|
tfa-cli claim mint --seed <projectId> [--emails a@x,b@y] [--ttl <sec>] # Mint /claim/<token> links for a project you own
|
|
44631
46191
|
tfa-cli next [--direction "<text>"] # Analyzer growth recommendation cards
|
|
44632
46192
|
tfa-cli <command> help # Details per command
|
|
@@ -44707,7 +46267,7 @@ var WORD_FLAGS = {
|
|
|
44707
46267
|
agents: "list-agents",
|
|
44708
46268
|
config: "show-config"
|
|
44709
46269
|
};
|
|
44710
|
-
var HELP_SUBCOMMANDS = ["agent", "todo", "project", "brand", "device", "list", "ls", "status", "show", "open"];
|
|
46270
|
+
var HELP_SUBCOMMANDS = ["agent", "todo", "project", "brand", "device", "list", "ls", "status", "show", "open", "inbox", "notify"];
|
|
44711
46271
|
function parseCliArgs() {
|
|
44712
46272
|
const { values, positionals } = parseArgs({
|
|
44713
46273
|
args: process.argv.slice(2),
|
|
@@ -44742,6 +46302,10 @@ function parseCliArgs() {
|
|
|
44742
46302
|
seed: { type: "string" },
|
|
44743
46303
|
emails: { type: "string" },
|
|
44744
46304
|
ttl: { type: "string" },
|
|
46305
|
+
subject: { type: "string" },
|
|
46306
|
+
href: { type: "string" },
|
|
46307
|
+
unread: { type: "boolean", default: false },
|
|
46308
|
+
limit: { type: "string" },
|
|
44745
46309
|
resume: { type: "string", short: "r" },
|
|
44746
46310
|
continue: { type: "boolean", short: "c", default: false },
|
|
44747
46311
|
"non-interactive": { type: "boolean", short: "n", default: false },
|
|
@@ -46999,7 +48563,88 @@ function renderDiff(originalContent, modifiedContent, filePath) {
|
|
|
46999
48563
|
return parts.join("");
|
|
47000
48564
|
}
|
|
47001
48565
|
|
|
48566
|
+
// src/shutdown.ts
|
|
48567
|
+
var activeRun = null;
|
|
48568
|
+
var cleanups = [];
|
|
48569
|
+
var exiting = null;
|
|
48570
|
+
var cancelling = null;
|
|
48571
|
+
var CANCEL_TIMEOUT_MS = 5000;
|
|
48572
|
+
function setActiveRun(run) {
|
|
48573
|
+
activeRun = run;
|
|
48574
|
+
}
|
|
48575
|
+
function withTimeout(p, ms, fallback) {
|
|
48576
|
+
let timer;
|
|
48577
|
+
const timeout = new Promise((r) => {
|
|
48578
|
+
timer = setTimeout(() => r(fallback), ms);
|
|
48579
|
+
});
|
|
48580
|
+
return Promise.race([p, timeout]).finally(() => clearTimeout(timer));
|
|
48581
|
+
}
|
|
48582
|
+
function cancelActiveRun(timeoutMs = CANCEL_TIMEOUT_MS) {
|
|
48583
|
+
if (cancelling)
|
|
48584
|
+
return cancelling;
|
|
48585
|
+
const pending = activeRun;
|
|
48586
|
+
if (!pending)
|
|
48587
|
+
return Promise.resolve({ ok: false, todoId: null });
|
|
48588
|
+
const res = { ok: false };
|
|
48589
|
+
const attempt = (async () => {
|
|
48590
|
+
const run = await pending;
|
|
48591
|
+
res.todoId = run?.todoId ?? null;
|
|
48592
|
+
if (!run)
|
|
48593
|
+
return res;
|
|
48594
|
+
if (!run.ws.connected && !await run.ws.connect())
|
|
48595
|
+
return res;
|
|
48596
|
+
res.ok = await run.ws.sendInterrupt(run.projectId, run.todoId);
|
|
48597
|
+
return res;
|
|
48598
|
+
})().catch(() => res);
|
|
48599
|
+
cancelling = withTimeout(attempt, timeoutMs, res).finally(() => {
|
|
48600
|
+
if (activeRun === pending)
|
|
48601
|
+
activeRun = null;
|
|
48602
|
+
cancelling = null;
|
|
48603
|
+
});
|
|
48604
|
+
return cancelling;
|
|
48605
|
+
}
|
|
48606
|
+
function shutdown(code, reason) {
|
|
48607
|
+
return exiting ??= (async () => {
|
|
48608
|
+
if (reason)
|
|
48609
|
+
process.stderr.write(`
|
|
48610
|
+
${reason}
|
|
48611
|
+
`);
|
|
48612
|
+
if (activeRun) {
|
|
48613
|
+
const { ok, todoId } = await cancelActiveRun();
|
|
48614
|
+
if (ok)
|
|
48615
|
+
process.stderr.write(`Stopped todo ${todoId}
|
|
48616
|
+
`);
|
|
48617
|
+
else if (todoId)
|
|
48618
|
+
process.stderr.write(`Warning: could not stop todo ${todoId} (backend unreachable)
|
|
48619
|
+
`);
|
|
48620
|
+
else if (todoId === undefined)
|
|
48621
|
+
process.stderr.write(`Warning: could not stop the todo being created (timed out)
|
|
48622
|
+
`);
|
|
48623
|
+
}
|
|
48624
|
+
for (const fn of cleanups.splice(0)) {
|
|
48625
|
+
try {
|
|
48626
|
+
fn();
|
|
48627
|
+
} catch {}
|
|
48628
|
+
}
|
|
48629
|
+
process.exit(code);
|
|
48630
|
+
})();
|
|
48631
|
+
}
|
|
48632
|
+
|
|
47002
48633
|
// src/watch.ts
|
|
48634
|
+
var RECONNECT_WINDOW_MS = 60000;
|
|
48635
|
+
var DISCONNECTED = { type: "socket:closed", success: false };
|
|
48636
|
+
async function resubscribe(ws, todoId, callback, windowMs = RECONNECT_WINDOW_MS) {
|
|
48637
|
+
const deadline = Date.now() + windowMs;
|
|
48638
|
+
for (let delay = 1000;; delay = Math.min(delay * 2, 5000)) {
|
|
48639
|
+
await new Promise((r) => setTimeout(r, Math.max(0, Math.min(delay, deadline - Date.now()))));
|
|
48640
|
+
const left = deadline - Date.now();
|
|
48641
|
+
if (left <= 0)
|
|
48642
|
+
return false;
|
|
48643
|
+
const attempt = ws.subscribe(todoId, callback, { replayStatus: true }).catch(() => false);
|
|
48644
|
+
if (await withTimeout(attempt, left, false))
|
|
48645
|
+
return true;
|
|
48646
|
+
}
|
|
48647
|
+
}
|
|
47003
48648
|
var diffStoreByWs = new WeakMap;
|
|
47004
48649
|
function classifyBlock(info) {
|
|
47005
48650
|
const inner = (info.block_type || "").toLowerCase();
|
|
@@ -47081,12 +48726,8 @@ async function watchTodo(ws, todoId, projectId, opts = {}) {
|
|
|
47081
48726
|
process.removeAllListeners("SIGINT");
|
|
47082
48727
|
process.on("SIGINT", () => {
|
|
47083
48728
|
interruptCount++;
|
|
47084
|
-
if (interruptCount >= 2)
|
|
47085
|
-
|
|
47086
|
-
${RED}Force exit (double Ctrl+C)${RESET}
|
|
47087
|
-
`);
|
|
47088
|
-
process.exit(130);
|
|
47089
|
-
}
|
|
48729
|
+
if (opts.exitOnInterrupt || interruptCount >= 2)
|
|
48730
|
+
return void shutdown(130, "Cancelled by user (Ctrl+C)");
|
|
47090
48731
|
process.stderr.write(`
|
|
47091
48732
|
${YELLOW}Interrupting... (Ctrl+C again to force exit)${RESET}
|
|
47092
48733
|
`);
|
|
@@ -47285,8 +48926,22 @@ ${DIM}[todo:status] ${status}${RESET}
|
|
|
47285
48926
|
callback(msgType, payload);
|
|
47286
48927
|
}
|
|
47287
48928
|
}
|
|
48929
|
+
setActiveRun({ ws, projectId, todoId });
|
|
47288
48930
|
try {
|
|
47289
|
-
const
|
|
48931
|
+
const subscribed = await ws.subscribe(todoId, callback).catch(() => false);
|
|
48932
|
+
let result = subscribed ? await ws.completion(todoId) : DISCONNECTED;
|
|
48933
|
+
while (result?.type === "socket:closed") {
|
|
48934
|
+
process.stderr.write(`
|
|
48935
|
+
${YELLOW}Connection lost — reconnecting...${RESET}
|
|
48936
|
+
`);
|
|
48937
|
+
if (!await resubscribe(ws, todoId, callback)) {
|
|
48938
|
+
await shutdown(1, `${RED}Error: backend unreachable for ${RECONNECT_WINDOW_MS / 1000}s — giving up on todo ${todoId}${RESET}`);
|
|
48939
|
+
}
|
|
48940
|
+
process.stderr.write(`${DIM}Reconnected.${RESET}
|
|
48941
|
+
`);
|
|
48942
|
+
result = await ws.completion(todoId);
|
|
48943
|
+
}
|
|
48944
|
+
setActiveRun(null);
|
|
47290
48945
|
process.stdout.write(`
|
|
47291
48946
|
`);
|
|
47292
48947
|
if (!result?.success) {
|
|
@@ -47299,6 +48954,8 @@ ${DIM}[todo:status] ${status}${RESET}
|
|
|
47299
48954
|
}
|
|
47300
48955
|
return true;
|
|
47301
48956
|
} catch (e) {
|
|
48957
|
+
await cancelActiveRun();
|
|
48958
|
+
process.exitCode = 1;
|
|
47302
48959
|
if (!opts.suppressCancelNotice) {
|
|
47303
48960
|
process.stderr.write(`${YELLOW}Interrupted${RESET}
|
|
47304
48961
|
`);
|
|
@@ -48362,210 +50019,629 @@ async function deviceCommand(api, positionals, args) {
|
|
|
48362
50019
|
await api.renameDevice(found.id, value);
|
|
48363
50020
|
process.stderr.write(`${GREEN}✅ ${found.name} → ${value}${RESET}
|
|
48364
50021
|
`);
|
|
48365
|
-
return;
|
|
50022
|
+
return;
|
|
50023
|
+
}
|
|
50024
|
+
if (sub === "wallpaper") {
|
|
50025
|
+
if (value !== "auto" && value !== "off")
|
|
50026
|
+
fail("Usage: tfa-cli device wallpaper <device> auto|off");
|
|
50027
|
+
await api.setDeviceWallpaperMode(found.id, value);
|
|
50028
|
+
process.stderr.write(`${GREEN}✅ ${found.name} wallpaper: ${value}${RESET}
|
|
50029
|
+
`);
|
|
50030
|
+
return;
|
|
50031
|
+
}
|
|
50032
|
+
fail(`Unknown 'device' subcommand: ${sub}`);
|
|
50033
|
+
}
|
|
50034
|
+
|
|
50035
|
+
// src/list-todos.ts
|
|
50036
|
+
import { parseArgs as parseArgs2 } from "util";
|
|
50037
|
+
var STATUS_COLOR = {
|
|
50038
|
+
DONE: GREEN,
|
|
50039
|
+
READY: GREEN,
|
|
50040
|
+
READY_CHECKED: GREEN,
|
|
50041
|
+
ERROR: RED,
|
|
50042
|
+
ERROR_CHECKED: RED,
|
|
50043
|
+
CANCELLED: RED,
|
|
50044
|
+
CANCELLED_CHECKED: RED,
|
|
50045
|
+
RUNNING: YELLOW,
|
|
50046
|
+
STOPPING: YELLOW,
|
|
50047
|
+
COMPACTING: YELLOW,
|
|
50048
|
+
REVIEW_REQUESTED: YELLOW,
|
|
50049
|
+
TODO: CYAN,
|
|
50050
|
+
SCHEDULED: CYAN,
|
|
50051
|
+
POSTPONED: CYAN,
|
|
50052
|
+
PAUSED: CYAN
|
|
50053
|
+
};
|
|
50054
|
+
var CLOSED = new Set([
|
|
50055
|
+
"DONE",
|
|
50056
|
+
"READY_CHECKED",
|
|
50057
|
+
"CANCELLED",
|
|
50058
|
+
"CANCELLED_CHECKED",
|
|
50059
|
+
"ERROR_CHECKED",
|
|
50060
|
+
"ARCHIVED",
|
|
50061
|
+
"DELETED"
|
|
50062
|
+
]);
|
|
50063
|
+
var VALID_STATUS = new Set(Object.values(TodoStatus));
|
|
50064
|
+
var isOpen = (s) => !CLOSED.has(s);
|
|
50065
|
+
function printListTodosHelp() {
|
|
50066
|
+
process.stderr.write(`
|
|
50067
|
+
todoforai-cli list — list todos in a project (recent first)
|
|
50068
|
+
|
|
50069
|
+
Usage:
|
|
50070
|
+
todoforai-cli list [flags]
|
|
50071
|
+
|
|
50072
|
+
Flags:
|
|
50073
|
+
-n, --limit <n> Max rows to show (default: 30)
|
|
50074
|
+
--cursor <n> Fetch todos older than this cursor (lastActivityAt)
|
|
50075
|
+
--page-size <n> Backend page size per request (default: min(limit, 100), max: 100)
|
|
50076
|
+
--search <text> Search content/status on the backend
|
|
50077
|
+
-s, --status <S[,S2]> Filter by status (comma-separated, union).
|
|
50078
|
+
-A, --all Include DONE (also CANCELLED, ARCHIVED, …)
|
|
50079
|
+
--project <id> Project ID (default: current default project)
|
|
50080
|
+
--json Output { items, nextCursor } as JSON
|
|
50081
|
+
-h, --help Show this help
|
|
50082
|
+
|
|
50083
|
+
Examples:
|
|
50084
|
+
todoforai-cli list # 30 most recent open todos
|
|
50085
|
+
todoforai-cli list -n 50 # last 50 open
|
|
50086
|
+
todoforai-cli list --cursor 1719234567890 # next page
|
|
50087
|
+
todoforai-cli list --all # include DONE
|
|
50088
|
+
todoforai-cli list -s RUNNING,REVIEW_REQUESTED
|
|
50089
|
+
todoforai-cli list --search bug --json | jq '.items[].id'
|
|
50090
|
+
`);
|
|
50091
|
+
}
|
|
50092
|
+
function positiveInt(value, name) {
|
|
50093
|
+
if (value === undefined)
|
|
50094
|
+
return;
|
|
50095
|
+
const n = Number(value);
|
|
50096
|
+
if (!Number.isFinite(n) || !Number.isInteger(n) || n < 1) {
|
|
50097
|
+
process.stderr.write(`${RED}Error: ${name} must be a positive integer${RESET}
|
|
50098
|
+
`);
|
|
50099
|
+
process.exit(2);
|
|
50100
|
+
}
|
|
50101
|
+
return n;
|
|
50102
|
+
}
|
|
50103
|
+
function cursorValue(value) {
|
|
50104
|
+
if (value === undefined)
|
|
50105
|
+
return;
|
|
50106
|
+
const n = Number(value);
|
|
50107
|
+
if (!Number.isFinite(n) || n < 0) {
|
|
50108
|
+
process.stderr.write(`${RED}Error: --cursor must be a non-negative number${RESET}
|
|
50109
|
+
`);
|
|
50110
|
+
process.exit(2);
|
|
50111
|
+
}
|
|
50112
|
+
return n;
|
|
50113
|
+
}
|
|
50114
|
+
function toItemsPage(response) {
|
|
50115
|
+
if (Array.isArray(response))
|
|
50116
|
+
return { items: response };
|
|
50117
|
+
return {
|
|
50118
|
+
items: Array.isArray(response?.items) ? response.items : [],
|
|
50119
|
+
nextCursor: typeof response?.nextCursor === "number" ? response.nextCursor : undefined
|
|
50120
|
+
};
|
|
50121
|
+
}
|
|
50122
|
+
function rowTime(t) {
|
|
50123
|
+
return Number(t.lastActivityAt ?? t.createdAt ?? 0);
|
|
50124
|
+
}
|
|
50125
|
+
function printStatusError(unknown) {
|
|
50126
|
+
process.stderr.write(`${RED}Error: unknown status ${unknown.map((s) => `'${s}'`).join(", ")}${RESET}
|
|
50127
|
+
`);
|
|
50128
|
+
process.stderr.write(`${DIM}Use OPEN or one of: ${Object.values(TodoStatus).join(", ")}${RESET}
|
|
50129
|
+
`);
|
|
50130
|
+
process.exit(2);
|
|
50131
|
+
}
|
|
50132
|
+
function matchesRequestedStatus(t, requested, wanted, wantOpen) {
|
|
50133
|
+
if (!requested)
|
|
50134
|
+
return true;
|
|
50135
|
+
const s = String(t.status).toUpperCase();
|
|
50136
|
+
return wantOpen && isOpen(s) || (wanted?.has(s) ?? false);
|
|
50137
|
+
}
|
|
50138
|
+
async function listTodosCommand(api, defaultProjectId, argv) {
|
|
50139
|
+
const { values } = parseArgs2({
|
|
50140
|
+
args: argv,
|
|
50141
|
+
options: {
|
|
50142
|
+
limit: { type: "string", short: "n" },
|
|
50143
|
+
cursor: { type: "string" },
|
|
50144
|
+
"page-size": { type: "string" },
|
|
50145
|
+
search: { type: "string" },
|
|
50146
|
+
status: { type: "string", short: "s" },
|
|
50147
|
+
all: { type: "boolean", short: "A", default: false },
|
|
50148
|
+
project: { type: "string" },
|
|
50149
|
+
json: { type: "boolean", default: false },
|
|
50150
|
+
help: { type: "boolean", short: "h", default: false }
|
|
50151
|
+
},
|
|
50152
|
+
strict: true
|
|
50153
|
+
});
|
|
50154
|
+
if (values.help) {
|
|
50155
|
+
printListTodosHelp();
|
|
50156
|
+
return;
|
|
50157
|
+
}
|
|
50158
|
+
const projectId = values.project || defaultProjectId;
|
|
50159
|
+
if (!projectId) {
|
|
50160
|
+
process.stderr.write(`${RED}Error: no project (pass --project <id> or set a default)${RESET}
|
|
50161
|
+
`);
|
|
50162
|
+
process.exit(1);
|
|
50163
|
+
}
|
|
50164
|
+
const limit = positiveInt(values.limit, "--limit") ?? 30;
|
|
50165
|
+
const pageSize = Math.min(positiveInt(values["page-size"], "--page-size") ?? Math.min(Math.max(limit, 1), 100), 100);
|
|
50166
|
+
let cursor = cursorValue(values.cursor);
|
|
50167
|
+
const search = values.search ? String(values.search) : undefined;
|
|
50168
|
+
const requested = values.status ? String(values.status).split(",").map((s) => s.trim().toUpperCase()).filter(Boolean) : null;
|
|
50169
|
+
const unknown = requested?.filter((s) => s !== "OPEN" && !VALID_STATUS.has(s)) ?? [];
|
|
50170
|
+
if (unknown.length)
|
|
50171
|
+
printStatusError(unknown);
|
|
50172
|
+
const wantOpen = !!requested?.some((s) => s === "OPEN");
|
|
50173
|
+
const wanted = requested ? new Set(requested.filter((s) => VALID_STATUS.has(s))) : null;
|
|
50174
|
+
const includeClosed = !!values.all || !!wanted && wanted.size > 0;
|
|
50175
|
+
const rows = [];
|
|
50176
|
+
let backendNextCursor;
|
|
50177
|
+
while (rows.length < limit) {
|
|
50178
|
+
const requestLimit = Math.min(pageSize, limit - rows.length);
|
|
50179
|
+
const page = toItemsPage(await api.listTodos(projectId, { limit: requestLimit, cursor, search }));
|
|
50180
|
+
backendNextCursor = page.nextCursor;
|
|
50181
|
+
for (const t of page.items) {
|
|
50182
|
+
const status = String(t.status).toUpperCase();
|
|
50183
|
+
if (!includeClosed && CLOSED.has(status))
|
|
50184
|
+
continue;
|
|
50185
|
+
if (!matchesRequestedStatus(t, requested, wanted, wantOpen))
|
|
50186
|
+
continue;
|
|
50187
|
+
rows.push(t);
|
|
50188
|
+
if (rows.length >= limit)
|
|
50189
|
+
break;
|
|
50190
|
+
}
|
|
50191
|
+
if (!backendNextCursor || page.items.length === 0)
|
|
50192
|
+
break;
|
|
50193
|
+
cursor = backendNextCursor;
|
|
50194
|
+
}
|
|
50195
|
+
rows.sort((a, b) => rowTime(b) - rowTime(a));
|
|
50196
|
+
const visible = rows.slice(0, limit);
|
|
50197
|
+
const hasBufferedRows = rows.length > visible.length;
|
|
50198
|
+
const nextCursor = visible.length && (backendNextCursor !== undefined || hasBufferedRows) ? rowTime(visible[visible.length - 1]) : undefined;
|
|
50199
|
+
if (values.json) {
|
|
50200
|
+
process.stdout.write(JSON.stringify({ items: visible, nextCursor }, null, 2) + `
|
|
50201
|
+
`);
|
|
50202
|
+
return;
|
|
50203
|
+
}
|
|
50204
|
+
if (!visible.length) {
|
|
50205
|
+
process.stderr.write(`${DIM}(no todos)${RESET}
|
|
50206
|
+
`);
|
|
50207
|
+
if (nextCursor !== undefined)
|
|
50208
|
+
process.stderr.write(`${DIM}Next cursor: ${nextCursor} (re-run with the same filters plus --cursor ${nextCursor})${RESET}
|
|
50209
|
+
`);
|
|
50210
|
+
return;
|
|
50211
|
+
}
|
|
50212
|
+
const statusW = visible.reduce((n, r) => Math.max(n, String(r.status).length), 0);
|
|
50213
|
+
for (const t of visible) {
|
|
50214
|
+
const status = String(t.status).toUpperCase();
|
|
50215
|
+
const sc = STATUS_COLOR[status] || DIM;
|
|
50216
|
+
const ts = new Date(rowTime(t)).toISOString().slice(0, 16).replace("T", " ");
|
|
50217
|
+
const title = String(t.content ?? "").split(`
|
|
50218
|
+
`)[0].slice(0, 100);
|
|
50219
|
+
process.stdout.write(`${sc}${status.padEnd(statusW)}${RESET} ${DIM}${ts}${RESET} ${t.id} ${title}
|
|
50220
|
+
`);
|
|
50221
|
+
}
|
|
50222
|
+
process.stderr.write(`${DIM}${visible.length} todo(s)${RESET}
|
|
50223
|
+
`);
|
|
50224
|
+
if (nextCursor !== undefined)
|
|
50225
|
+
process.stderr.write(`${DIM}Next cursor: ${nextCursor} (re-run with the same filters plus --cursor ${nextCursor})${RESET}
|
|
50226
|
+
`);
|
|
50227
|
+
}
|
|
50228
|
+
|
|
50229
|
+
// src/notify-command.ts
|
|
50230
|
+
import { createHash } from "crypto";
|
|
50231
|
+
|
|
50232
|
+
// src/input.ts
|
|
50233
|
+
function readMultiline2(prompt, history) {
|
|
50234
|
+
let cancelFn = () => {};
|
|
50235
|
+
const promise = new Promise((resolve4, reject) => {
|
|
50236
|
+
const out = process.stderr;
|
|
50237
|
+
let buf = "";
|
|
50238
|
+
let cursor = 0;
|
|
50239
|
+
let pasting = false;
|
|
50240
|
+
let done = false;
|
|
50241
|
+
let screenRow = 0;
|
|
50242
|
+
let historyIndex = -1;
|
|
50243
|
+
let savedCurrent = "";
|
|
50244
|
+
const promptLen = prompt.replace(/\x1b\[[0-9;]*m/g, "").length;
|
|
50245
|
+
out.write(prompt);
|
|
50246
|
+
process.stdin.setRawMode(true);
|
|
50247
|
+
process.stdin.resume();
|
|
50248
|
+
out.write("\x1B[?2004h");
|
|
50249
|
+
function rowOf(pos) {
|
|
50250
|
+
const cols = process.stderr.columns || 80;
|
|
50251
|
+
const lines = buf.slice(0, pos).split(`
|
|
50252
|
+
`);
|
|
50253
|
+
let row = 0;
|
|
50254
|
+
for (let i = 0;i < lines.length; i++) {
|
|
50255
|
+
const len = (i === 0 ? promptLen : 0) + lines[i].length;
|
|
50256
|
+
if (i < lines.length - 1) {
|
|
50257
|
+
row += len === 0 ? 1 : Math.ceil(len / cols);
|
|
50258
|
+
} else {
|
|
50259
|
+
row += len > 0 ? Math.ceil(len / cols) - 1 : 0;
|
|
50260
|
+
}
|
|
50261
|
+
}
|
|
50262
|
+
return row;
|
|
50263
|
+
}
|
|
50264
|
+
function colOf(pos) {
|
|
50265
|
+
const cols = process.stderr.columns || 80;
|
|
50266
|
+
const lastNl = buf.lastIndexOf(`
|
|
50267
|
+
`, pos - 1);
|
|
50268
|
+
const lineStart = lastNl + 1;
|
|
50269
|
+
const offset = lineStart === 0 ? promptLen : 0;
|
|
50270
|
+
const linePos = offset + (pos - lineStart);
|
|
50271
|
+
if (linePos > 0 && linePos % cols === 0)
|
|
50272
|
+
return cols;
|
|
50273
|
+
return linePos % cols;
|
|
50274
|
+
}
|
|
50275
|
+
function totalRows() {
|
|
50276
|
+
const cols = process.stderr.columns || 80;
|
|
50277
|
+
const lines = buf.split(`
|
|
50278
|
+
`);
|
|
50279
|
+
let rows = 0;
|
|
50280
|
+
for (let i = 0;i < lines.length; i++) {
|
|
50281
|
+
const len = (i === 0 ? promptLen : 0) + lines[i].length;
|
|
50282
|
+
rows += len === 0 ? 1 : Math.ceil(len / cols);
|
|
50283
|
+
}
|
|
50284
|
+
return rows;
|
|
50285
|
+
}
|
|
50286
|
+
function redraw() {
|
|
50287
|
+
const cols = process.stderr.columns || 80;
|
|
50288
|
+
out.write("\r");
|
|
50289
|
+
if (screenRow > 0)
|
|
50290
|
+
out.write(`\x1B[${screenRow}A`);
|
|
50291
|
+
out.write("\x1B[J");
|
|
50292
|
+
out.write(prompt + buf.replace(/\n/g, `\r
|
|
50293
|
+
`));
|
|
50294
|
+
const targetRow = rowOf(cursor);
|
|
50295
|
+
const endRow = totalRows() - 1;
|
|
50296
|
+
const rowsBack = endRow - targetRow;
|
|
50297
|
+
const col = colOf(cursor);
|
|
50298
|
+
if (rowsBack > 0)
|
|
50299
|
+
out.write(`\x1B[${rowsBack}A`);
|
|
50300
|
+
if (col <= cols) {
|
|
50301
|
+
out.write("\r");
|
|
50302
|
+
if (col > 0 && col < cols)
|
|
50303
|
+
out.write(`\x1B[${col}C`);
|
|
50304
|
+
if (col === cols)
|
|
50305
|
+
out.write(`\x1B[${cols}G`);
|
|
50306
|
+
}
|
|
50307
|
+
screenRow = targetRow;
|
|
50308
|
+
}
|
|
50309
|
+
function finish(cancelled) {
|
|
50310
|
+
if (done)
|
|
50311
|
+
return;
|
|
50312
|
+
done = true;
|
|
50313
|
+
const rowsDown = totalRows() - 1 - screenRow;
|
|
50314
|
+
if (rowsDown > 0)
|
|
50315
|
+
out.write(`\x1B[${rowsDown}B`);
|
|
50316
|
+
out.write("\x1B[?2004l");
|
|
50317
|
+
out.write(`
|
|
50318
|
+
`);
|
|
50319
|
+
process.stdin.setRawMode(false);
|
|
50320
|
+
process.stdin.pause();
|
|
50321
|
+
process.stdin.removeListener("data", onData);
|
|
50322
|
+
if (cancelled)
|
|
50323
|
+
reject(new Error("cancelled"));
|
|
50324
|
+
else
|
|
50325
|
+
resolve4(buf.trim());
|
|
50326
|
+
}
|
|
50327
|
+
function wordRight() {
|
|
50328
|
+
let p = cursor;
|
|
50329
|
+
while (p < buf.length && buf[p] === " ")
|
|
50330
|
+
p++;
|
|
50331
|
+
while (p < buf.length && buf[p] !== " ")
|
|
50332
|
+
p++;
|
|
50333
|
+
return p;
|
|
50334
|
+
}
|
|
50335
|
+
function wordLeft() {
|
|
50336
|
+
let p = cursor;
|
|
50337
|
+
while (p > 0 && buf[p - 1] === " ")
|
|
50338
|
+
p--;
|
|
50339
|
+
while (p > 0 && buf[p - 1] !== " ")
|
|
50340
|
+
p--;
|
|
50341
|
+
return p;
|
|
50342
|
+
}
|
|
50343
|
+
function killToEnd() {
|
|
50344
|
+
buf = buf.slice(0, cursor);
|
|
50345
|
+
redraw();
|
|
50346
|
+
}
|
|
50347
|
+
function deleteWordBack() {
|
|
50348
|
+
const to = wordLeft();
|
|
50349
|
+
if (to === cursor)
|
|
50350
|
+
return;
|
|
50351
|
+
buf = buf.slice(0, to) + buf.slice(cursor);
|
|
50352
|
+
cursor = to;
|
|
50353
|
+
redraw();
|
|
50354
|
+
}
|
|
50355
|
+
function handleCSI(chunk, start) {
|
|
50356
|
+
let i = start + 1;
|
|
50357
|
+
if (i >= chunk.length || chunk[i] !== "[") {
|
|
50358
|
+
if (i < chunk.length)
|
|
50359
|
+
i++;
|
|
50360
|
+
return i;
|
|
50361
|
+
}
|
|
50362
|
+
i++;
|
|
50363
|
+
let params = "";
|
|
50364
|
+
while (i < chunk.length && /[0-9;]/.test(chunk[i])) {
|
|
50365
|
+
params += chunk[i];
|
|
50366
|
+
i++;
|
|
50367
|
+
}
|
|
50368
|
+
const final = i < chunk.length ? chunk[i] : "";
|
|
50369
|
+
i++;
|
|
50370
|
+
const parts = params.split(";");
|
|
50371
|
+
const modifier = parts.length > 1 ? parseInt(parts[1]) : 0;
|
|
50372
|
+
const code = parts[0] || "";
|
|
50373
|
+
const ctrl = modifier === 5;
|
|
50374
|
+
switch (final) {
|
|
50375
|
+
case "D":
|
|
50376
|
+
if (ctrl) {
|
|
50377
|
+
cursor = wordLeft();
|
|
50378
|
+
redraw();
|
|
50379
|
+
} else if (cursor > 0) {
|
|
50380
|
+
cursor--;
|
|
50381
|
+
redraw();
|
|
50382
|
+
}
|
|
50383
|
+
break;
|
|
50384
|
+
case "C":
|
|
50385
|
+
if (ctrl) {
|
|
50386
|
+
cursor = wordRight();
|
|
50387
|
+
redraw();
|
|
50388
|
+
} else if (cursor < buf.length) {
|
|
50389
|
+
cursor++;
|
|
50390
|
+
redraw();
|
|
50391
|
+
}
|
|
50392
|
+
break;
|
|
50393
|
+
case "A":
|
|
50394
|
+
if (history && history.length > 0) {
|
|
50395
|
+
if (historyIndex === -1)
|
|
50396
|
+
savedCurrent = buf;
|
|
50397
|
+
if (historyIndex < history.length - 1) {
|
|
50398
|
+
historyIndex++;
|
|
50399
|
+
buf = history[history.length - 1 - historyIndex];
|
|
50400
|
+
cursor = buf.length;
|
|
50401
|
+
redraw();
|
|
50402
|
+
}
|
|
50403
|
+
}
|
|
50404
|
+
break;
|
|
50405
|
+
case "B":
|
|
50406
|
+
if (history && historyIndex >= 0) {
|
|
50407
|
+
historyIndex--;
|
|
50408
|
+
if (historyIndex === -1) {
|
|
50409
|
+
buf = savedCurrent;
|
|
50410
|
+
} else {
|
|
50411
|
+
buf = history[history.length - 1 - historyIndex];
|
|
50412
|
+
}
|
|
50413
|
+
cursor = buf.length;
|
|
50414
|
+
redraw();
|
|
50415
|
+
}
|
|
50416
|
+
break;
|
|
50417
|
+
case "H":
|
|
50418
|
+
cursor = 0;
|
|
50419
|
+
redraw();
|
|
50420
|
+
break;
|
|
50421
|
+
case "F":
|
|
50422
|
+
cursor = buf.length;
|
|
50423
|
+
redraw();
|
|
50424
|
+
break;
|
|
50425
|
+
case "~":
|
|
50426
|
+
if (code === "3" && cursor < buf.length) {
|
|
50427
|
+
buf = buf.slice(0, cursor) + buf.slice(cursor + 1);
|
|
50428
|
+
redraw();
|
|
50429
|
+
}
|
|
50430
|
+
break;
|
|
50431
|
+
}
|
|
50432
|
+
return i;
|
|
50433
|
+
}
|
|
50434
|
+
function onData(data) {
|
|
50435
|
+
let s = data.toString("utf-8");
|
|
50436
|
+
while (s.length > 0 && !done) {
|
|
50437
|
+
if (pasting) {
|
|
50438
|
+
const end = s.indexOf("\x1B[201~");
|
|
50439
|
+
if (end >= 0) {
|
|
50440
|
+
const text = s.slice(0, end).replace(/\r\n?|\n/g, `
|
|
50441
|
+
`);
|
|
50442
|
+
buf = buf.slice(0, cursor) + text + buf.slice(cursor);
|
|
50443
|
+
cursor += text.length;
|
|
50444
|
+
pasting = false;
|
|
50445
|
+
s = s.slice(end + 6);
|
|
50446
|
+
redraw();
|
|
50447
|
+
} else {
|
|
50448
|
+
const text = s.replace(/\r\n?|\n/g, `
|
|
50449
|
+
`);
|
|
50450
|
+
buf = buf.slice(0, cursor) + text + buf.slice(cursor);
|
|
50451
|
+
cursor += text.length;
|
|
50452
|
+
s = "";
|
|
50453
|
+
redraw();
|
|
50454
|
+
}
|
|
50455
|
+
} else {
|
|
50456
|
+
const ps = s.indexOf("\x1B[200~");
|
|
50457
|
+
const chunk = ps >= 0 ? s.slice(0, ps) : s;
|
|
50458
|
+
for (let i = 0;i < chunk.length && !done; i++) {
|
|
50459
|
+
const c3 = chunk.charCodeAt(i);
|
|
50460
|
+
if (c3 === 3) {
|
|
50461
|
+
finish(true);
|
|
50462
|
+
return;
|
|
50463
|
+
}
|
|
50464
|
+
if (c3 === 13 || c3 === 10) {
|
|
50465
|
+
finish(false);
|
|
50466
|
+
return;
|
|
50467
|
+
}
|
|
50468
|
+
if (c3 === 23) {
|
|
50469
|
+
deleteWordBack();
|
|
50470
|
+
} else if (c3 === 11) {
|
|
50471
|
+
killToEnd();
|
|
50472
|
+
} else if (c3 === 127 || c3 === 8) {
|
|
50473
|
+
if (cursor > 0) {
|
|
50474
|
+
buf = buf.slice(0, cursor - 1) + buf.slice(cursor);
|
|
50475
|
+
cursor--;
|
|
50476
|
+
redraw();
|
|
50477
|
+
}
|
|
50478
|
+
} else if (c3 === 27) {
|
|
50479
|
+
if (i + 1 < chunk.length && chunk.charCodeAt(i + 1) === 13) {
|
|
50480
|
+
buf = buf.slice(0, cursor) + `
|
|
50481
|
+
` + buf.slice(cursor);
|
|
50482
|
+
cursor++;
|
|
50483
|
+
i++;
|
|
50484
|
+
redraw();
|
|
50485
|
+
} else {
|
|
50486
|
+
i = handleCSI(chunk, i) - 1;
|
|
50487
|
+
}
|
|
50488
|
+
} else if (c3 === 1) {
|
|
50489
|
+
cursor = 0;
|
|
50490
|
+
redraw();
|
|
50491
|
+
} else if (c3 === 5) {
|
|
50492
|
+
cursor = buf.length;
|
|
50493
|
+
redraw();
|
|
50494
|
+
} else if (c3 >= 32) {
|
|
50495
|
+
buf = buf.slice(0, cursor) + chunk[i] + buf.slice(cursor);
|
|
50496
|
+
cursor++;
|
|
50497
|
+
redraw();
|
|
50498
|
+
}
|
|
50499
|
+
}
|
|
50500
|
+
if (ps >= 0) {
|
|
50501
|
+
pasting = true;
|
|
50502
|
+
s = s.slice(ps + 6);
|
|
50503
|
+
} else
|
|
50504
|
+
s = "";
|
|
50505
|
+
}
|
|
50506
|
+
}
|
|
50507
|
+
}
|
|
50508
|
+
process.stdin.on("data", onData);
|
|
50509
|
+
cancelFn = () => finish(true);
|
|
50510
|
+
});
|
|
50511
|
+
return { promise, cancel: () => cancelFn() };
|
|
50512
|
+
}
|
|
50513
|
+
async function readStdin2() {
|
|
50514
|
+
if (process.stdin.isTTY) {
|
|
50515
|
+
try {
|
|
50516
|
+
const content2 = await readMultiline2(`${BRIGHT_WHITE}TODO>${RESET} `).promise;
|
|
50517
|
+
if (!content2) {
|
|
50518
|
+
process.stderr.write(`Error: Empty input
|
|
50519
|
+
`);
|
|
50520
|
+
process.exit(1);
|
|
50521
|
+
}
|
|
50522
|
+
return content2;
|
|
50523
|
+
} catch {
|
|
50524
|
+
process.stderr.write(`
|
|
50525
|
+
Cancelled
|
|
50526
|
+
`);
|
|
50527
|
+
process.exit(1);
|
|
50528
|
+
}
|
|
48366
50529
|
}
|
|
48367
|
-
|
|
48368
|
-
|
|
48369
|
-
|
|
48370
|
-
|
|
48371
|
-
|
|
50530
|
+
const chunks = [];
|
|
50531
|
+
for await (const chunk of process.stdin)
|
|
50532
|
+
chunks.push(chunk);
|
|
50533
|
+
const content = Buffer.concat(chunks).toString("utf-8").trim();
|
|
50534
|
+
if (!content) {
|
|
50535
|
+
process.stderr.write(`Error: Empty input
|
|
48372
50536
|
`);
|
|
48373
|
-
|
|
50537
|
+
process.exit(1);
|
|
48374
50538
|
}
|
|
48375
|
-
|
|
50539
|
+
return content;
|
|
48376
50540
|
}
|
|
48377
50541
|
|
|
48378
|
-
// src/
|
|
48379
|
-
|
|
48380
|
-
|
|
48381
|
-
DONE: GREEN,
|
|
48382
|
-
READY: GREEN,
|
|
48383
|
-
READY_CHECKED: GREEN,
|
|
48384
|
-
ERROR: RED,
|
|
48385
|
-
ERROR_CHECKED: RED,
|
|
48386
|
-
CANCELLED: RED,
|
|
48387
|
-
CANCELLED_CHECKED: RED,
|
|
48388
|
-
RUNNING: YELLOW,
|
|
48389
|
-
STOPPING: YELLOW,
|
|
48390
|
-
COMPACTING: YELLOW,
|
|
48391
|
-
REVIEW_REQUESTED: YELLOW,
|
|
48392
|
-
TODO: CYAN,
|
|
48393
|
-
SCHEDULED: CYAN,
|
|
48394
|
-
POSTPONED: CYAN,
|
|
48395
|
-
PAUSED: CYAN
|
|
48396
|
-
};
|
|
48397
|
-
var CLOSED = new Set([
|
|
48398
|
-
"DONE",
|
|
48399
|
-
"READY_CHECKED",
|
|
48400
|
-
"CANCELLED",
|
|
48401
|
-
"CANCELLED_CHECKED",
|
|
48402
|
-
"ERROR_CHECKED",
|
|
48403
|
-
"ARCHIVED",
|
|
48404
|
-
"DELETED"
|
|
48405
|
-
]);
|
|
48406
|
-
var VALID_STATUS = new Set(Object.values(TodoStatus));
|
|
48407
|
-
var isOpen = (s) => !CLOSED.has(s);
|
|
48408
|
-
function printListTodosHelp() {
|
|
50542
|
+
// src/notify-command.ts
|
|
50543
|
+
var TIERS = { "needs-you": "priority", priority: "priority", activity: "activity", messages: "user_messages" };
|
|
50544
|
+
function printInboxHelp() {
|
|
48409
50545
|
process.stderr.write(`
|
|
48410
|
-
|
|
50546
|
+
tfa-cli inbox — the user's notification feed (the bell)
|
|
48411
50547
|
|
|
48412
50548
|
Usage:
|
|
48413
|
-
|
|
48414
|
-
|
|
48415
|
-
Flags:
|
|
48416
|
-
-n, --limit <n> Max rows to show (default: 30)
|
|
48417
|
-
--cursor <n> Fetch todos older than this cursor (lastActivityAt)
|
|
48418
|
-
--page-size <n> Backend page size per request (default: min(limit, 100), max: 100)
|
|
48419
|
-
--search <text> Search content/status on the backend
|
|
48420
|
-
-s, --status <S[,S2]> Filter by status (comma-separated, union).
|
|
48421
|
-
-A, --all Include DONE (also CANCELLED, ARCHIVED, …)
|
|
48422
|
-
--project <id> Project ID (default: current default project)
|
|
48423
|
-
--json Output { items, nextCursor } as JSON
|
|
48424
|
-
-h, --help Show this help
|
|
50549
|
+
tfa-cli inbox [needs-you|messages|activity] [--unread] [--limit 20] [--json]
|
|
50550
|
+
tfa-cli inbox seen [needs-you|messages|activity] Mark read (omit = everything)
|
|
48425
50551
|
|
|
48426
|
-
|
|
48427
|
-
|
|
48428
|
-
|
|
48429
|
-
|
|
48430
|
-
todoforai-cli list --all # include DONE
|
|
48431
|
-
todoforai-cli list -s RUNNING,REVIEW_REQUESTED
|
|
48432
|
-
todoforai-cli list --search bug --json | jq '.items[].id'
|
|
50552
|
+
Filters (same as the bell's chips):
|
|
50553
|
+
needs-you invites, offers, a todo waiting on the user, a failed todo (alias: priority)
|
|
50554
|
+
messages notes from the TODO for AI team or from an agent
|
|
50555
|
+
activity finished / recommended todos
|
|
48433
50556
|
`);
|
|
48434
50557
|
}
|
|
48435
|
-
function
|
|
48436
|
-
|
|
48437
|
-
|
|
48438
|
-
|
|
48439
|
-
|
|
48440
|
-
|
|
50558
|
+
function printNotifyHelp() {
|
|
50559
|
+
process.stderr.write(`
|
|
50560
|
+
tfa-cli notify — leave a note for YOUR user (their Messages tab + phone push)
|
|
50561
|
+
|
|
50562
|
+
Usage:
|
|
50563
|
+
tfa-cli notify "Title" "message" [--href /t/<todo-id>] [--subject <key>] [--json]
|
|
50564
|
+
echo "message" | tfa-cli notify "Title"
|
|
50565
|
+
|
|
50566
|
+
Recipient: ALWAYS the user this key belongs to (the person you work for). There is no --to:
|
|
50567
|
+
it cannot message a teammate, a customer or any other account. Shown as from your agent.
|
|
50568
|
+
--subject dedupes: the same subject is delivered once (default: hash of title+message,
|
|
50569
|
+
so a retry never double-notifies). --href must be an in-app path. Max 10 new notes/day.
|
|
48441
50570
|
`);
|
|
48442
|
-
process.exit(2);
|
|
48443
|
-
}
|
|
48444
|
-
return n;
|
|
48445
50571
|
}
|
|
48446
|
-
function
|
|
48447
|
-
if (
|
|
50572
|
+
function tierArg(name) {
|
|
50573
|
+
if (!name)
|
|
48448
50574
|
return;
|
|
48449
|
-
const
|
|
48450
|
-
if (!
|
|
48451
|
-
process.stderr.write(`${RED}
|
|
50575
|
+
const tier = TIERS[name];
|
|
50576
|
+
if (!tier) {
|
|
50577
|
+
process.stderr.write(`${RED}Unknown tab "${name}" — use needs-you, messages, activity${RESET}
|
|
48452
50578
|
`);
|
|
48453
50579
|
process.exit(2);
|
|
48454
50580
|
}
|
|
48455
|
-
return
|
|
48456
|
-
}
|
|
48457
|
-
function toItemsPage(response) {
|
|
48458
|
-
if (Array.isArray(response))
|
|
48459
|
-
return { items: response };
|
|
48460
|
-
return {
|
|
48461
|
-
items: Array.isArray(response?.items) ? response.items : [],
|
|
48462
|
-
nextCursor: typeof response?.nextCursor === "number" ? response.nextCursor : undefined
|
|
48463
|
-
};
|
|
50581
|
+
return tier;
|
|
48464
50582
|
}
|
|
48465
|
-
|
|
48466
|
-
|
|
48467
|
-
|
|
48468
|
-
|
|
48469
|
-
|
|
48470
|
-
|
|
48471
|
-
|
|
50583
|
+
var when = (ms) => new Date(ms).toLocaleString(undefined, { dateStyle: "short", timeStyle: "short" });
|
|
50584
|
+
async function inboxCommand(api, positionals, args) {
|
|
50585
|
+
const [, first, second] = positionals;
|
|
50586
|
+
if (first === "seen") {
|
|
50587
|
+
const tier2 = tierArg(second);
|
|
50588
|
+
await api.markFeedSeen(tier2 ? { tier: tier2 } : {});
|
|
50589
|
+
process.stderr.write(`${GREEN}✅ Marked ${second ?? "everything"} read${RESET}
|
|
48472
50590
|
`);
|
|
48473
|
-
process.exit(2);
|
|
48474
|
-
}
|
|
48475
|
-
function matchesRequestedStatus(t, requested, wanted, wantOpen) {
|
|
48476
|
-
if (!requested)
|
|
48477
|
-
return true;
|
|
48478
|
-
const s = String(t.status).toUpperCase();
|
|
48479
|
-
return wantOpen && isOpen(s) || (wanted?.has(s) ?? false);
|
|
48480
|
-
}
|
|
48481
|
-
async function listTodosCommand(api, defaultProjectId, argv) {
|
|
48482
|
-
const { values } = parseArgs2({
|
|
48483
|
-
args: argv,
|
|
48484
|
-
options: {
|
|
48485
|
-
limit: { type: "string", short: "n" },
|
|
48486
|
-
cursor: { type: "string" },
|
|
48487
|
-
"page-size": { type: "string" },
|
|
48488
|
-
search: { type: "string" },
|
|
48489
|
-
status: { type: "string", short: "s" },
|
|
48490
|
-
all: { type: "boolean", short: "A", default: false },
|
|
48491
|
-
project: { type: "string" },
|
|
48492
|
-
json: { type: "boolean", default: false },
|
|
48493
|
-
help: { type: "boolean", short: "h", default: false }
|
|
48494
|
-
},
|
|
48495
|
-
strict: true
|
|
48496
|
-
});
|
|
48497
|
-
if (values.help) {
|
|
48498
|
-
printListTodosHelp();
|
|
48499
50591
|
return;
|
|
48500
50592
|
}
|
|
48501
|
-
const
|
|
48502
|
-
|
|
48503
|
-
|
|
48504
|
-
|
|
48505
|
-
|
|
48506
|
-
}
|
|
48507
|
-
const limit = positiveInt(values.limit, "--limit") ?? 30;
|
|
48508
|
-
const pageSize = Math.min(positiveInt(values["page-size"], "--page-size") ?? Math.min(Math.max(limit, 1), 100), 100);
|
|
48509
|
-
let cursor = cursorValue(values.cursor);
|
|
48510
|
-
const search = values.search ? String(values.search) : undefined;
|
|
48511
|
-
const requested = values.status ? String(values.status).split(",").map((s) => s.trim().toUpperCase()).filter(Boolean) : null;
|
|
48512
|
-
const unknown = requested?.filter((s) => s !== "OPEN" && !VALID_STATUS.has(s)) ?? [];
|
|
48513
|
-
if (unknown.length)
|
|
48514
|
-
printStatusError(unknown);
|
|
48515
|
-
const wantOpen = !!requested?.some((s) => s === "OPEN");
|
|
48516
|
-
const wanted = requested ? new Set(requested.filter((s) => VALID_STATUS.has(s))) : null;
|
|
48517
|
-
const includeClosed = !!values.all || !!wanted && wanted.size > 0;
|
|
48518
|
-
const rows = [];
|
|
48519
|
-
let backendNextCursor;
|
|
48520
|
-
while (rows.length < limit) {
|
|
48521
|
-
const requestLimit = Math.min(pageSize, limit - rows.length);
|
|
48522
|
-
const page = toItemsPage(await api.listTodos(projectId, { limit: requestLimit, cursor, search }));
|
|
48523
|
-
backendNextCursor = page.nextCursor;
|
|
48524
|
-
for (const t of page.items) {
|
|
48525
|
-
const status = String(t.status).toUpperCase();
|
|
48526
|
-
if (!includeClosed && CLOSED.has(status))
|
|
48527
|
-
continue;
|
|
48528
|
-
if (!matchesRequestedStatus(t, requested, wanted, wantOpen))
|
|
48529
|
-
continue;
|
|
48530
|
-
rows.push(t);
|
|
48531
|
-
if (rows.length >= limit)
|
|
48532
|
-
break;
|
|
48533
|
-
}
|
|
48534
|
-
if (!backendNextCursor || page.items.length === 0)
|
|
48535
|
-
break;
|
|
48536
|
-
cursor = backendNextCursor;
|
|
48537
|
-
}
|
|
48538
|
-
rows.sort((a, b) => rowTime(b) - rowTime(a));
|
|
48539
|
-
const visible = rows.slice(0, limit);
|
|
48540
|
-
const hasBufferedRows = rows.length > visible.length;
|
|
48541
|
-
const nextCursor = visible.length && (backendNextCursor !== undefined || hasBufferedRows) ? rowTime(visible[visible.length - 1]) : undefined;
|
|
48542
|
-
if (values.json) {
|
|
48543
|
-
process.stdout.write(JSON.stringify({ items: visible, nextCursor }, null, 2) + `
|
|
48544
|
-
`);
|
|
50593
|
+
const tier = tierArg(first);
|
|
50594
|
+
const limit = args.limit ? Number(args.limit) : 20;
|
|
50595
|
+
const page = await api.listFeed({ tier, limit, sinceSeen: !!args.unread || undefined });
|
|
50596
|
+
if (args.json) {
|
|
50597
|
+
console.log(JSON.stringify(page));
|
|
48545
50598
|
return;
|
|
48546
50599
|
}
|
|
48547
|
-
|
|
48548
|
-
|
|
50600
|
+
const counts = page.unseenByTier;
|
|
50601
|
+
if (counts)
|
|
50602
|
+
process.stderr.write(`${DIM}unread — needs you ${counts.priority} · messages ${counts.user_messages} · activity ${counts.activity}${RESET}
|
|
48549
50603
|
`);
|
|
48550
|
-
|
|
48551
|
-
|
|
50604
|
+
if (!page.items.length) {
|
|
50605
|
+
process.stderr.write(`${DIM}Nothing here${RESET}
|
|
48552
50606
|
`);
|
|
48553
50607
|
return;
|
|
48554
50608
|
}
|
|
48555
|
-
|
|
48556
|
-
|
|
48557
|
-
const
|
|
48558
|
-
const
|
|
48559
|
-
|
|
48560
|
-
|
|
48561
|
-
`
|
|
48562
|
-
|
|
50609
|
+
for (const e of page.items) {
|
|
50610
|
+
const from = e.sender ? `${CYAN}${senderLabel(e.sender)}${RESET} ` : "";
|
|
50611
|
+
const dot = e.unread ? `${YELLOW}●${RESET}` : " ";
|
|
50612
|
+
const state = e.resolution ? ` ${DIM}[${e.resolution}]${RESET}` : "";
|
|
50613
|
+
console.log(`${dot} ${DIM}${when(e.createdAt)}${RESET} ${from}${BOLD}${e.title ?? e.message}${RESET}${state}`);
|
|
50614
|
+
if (e.title && e.message)
|
|
50615
|
+
console.log(` ${e.message.replace(/\n/g, `
|
|
50616
|
+
`)}`);
|
|
50617
|
+
if (e.href)
|
|
50618
|
+
console.log(` ${DIM}${e.href}${RESET}`);
|
|
50619
|
+
}
|
|
50620
|
+
}
|
|
50621
|
+
var RECIPIENT_FLAGS = ["--to", "--email", "--user", "--recipient", "--cc"];
|
|
50622
|
+
async function notifyCommand(api, positionals, args) {
|
|
50623
|
+
const argv = process.argv.slice(2);
|
|
50624
|
+
const bad = argv.find((a) => RECIPIENT_FLAGS.some((f) => a === f || a.startsWith(`${f}=`)));
|
|
50625
|
+
if (bad) {
|
|
50626
|
+
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}
|
|
48563
50627
|
`);
|
|
50628
|
+
process.exit(2);
|
|
48564
50629
|
}
|
|
48565
|
-
|
|
48566
|
-
|
|
48567
|
-
if (
|
|
48568
|
-
|
|
50630
|
+
const [, title, ...rest] = positionals;
|
|
50631
|
+
const message = rest.join(" ") || (process.stdin.isTTY ? "" : (await readStdin2()).trim());
|
|
50632
|
+
if (!title || !message) {
|
|
50633
|
+
printNotifyHelp();
|
|
50634
|
+
process.exit(2);
|
|
50635
|
+
}
|
|
50636
|
+
const subject = args.subject || createHash("sha256").update(`${title}
|
|
50637
|
+
${message}`).digest("hex").slice(0, 16);
|
|
50638
|
+
const row = await api.sendFeedMessage({ subject, title, message, ...args.href ? { href: String(args.href) } : {} });
|
|
50639
|
+
if (args.json) {
|
|
50640
|
+
console.log(JSON.stringify(row));
|
|
50641
|
+
return;
|
|
50642
|
+
}
|
|
50643
|
+
process.stderr.write(row.created !== false ? `${GREEN}✅ Delivered to your own user's Messages inbox (not anyone else)${RESET} ${DIM}(subject ${subject})${RESET}
|
|
50644
|
+
` : `${DIM}Already sent (subject ${subject}) — nothing new delivered${RESET}
|
|
48569
50645
|
`);
|
|
48570
50646
|
}
|
|
48571
50647
|
|
|
@@ -48833,7 +50909,7 @@ async function spawnMayflyBridge(apiUrl, todoId, workspace, opts = {}) {
|
|
|
48833
50909
|
}
|
|
48834
50910
|
});
|
|
48835
50911
|
});
|
|
48836
|
-
return { child, stop };
|
|
50912
|
+
return { child, stop, stopped: () => stopped };
|
|
48837
50913
|
}
|
|
48838
50914
|
|
|
48839
50915
|
// src/acp.ts
|
|
@@ -72898,6 +74974,93 @@ async function runAcp(apiUrl, apiKey, opts) {
|
|
|
72898
74974
|
await agent2.close();
|
|
72899
74975
|
}
|
|
72900
74976
|
|
|
74977
|
+
// src/shutdown.ts
|
|
74978
|
+
var activeRun2 = null;
|
|
74979
|
+
var cleanups2 = [];
|
|
74980
|
+
var exiting2 = null;
|
|
74981
|
+
var cancelling2 = null;
|
|
74982
|
+
var CANCEL_TIMEOUT_MS2 = 5000;
|
|
74983
|
+
function setActiveRun2(run2) {
|
|
74984
|
+
activeRun2 = run2;
|
|
74985
|
+
}
|
|
74986
|
+
function trackStart(ws, projectId, started) {
|
|
74987
|
+
setActiveRun2(started.then((todoId) => ({ ws, projectId, todoId }), () => null));
|
|
74988
|
+
}
|
|
74989
|
+
function onShutdown(fn) {
|
|
74990
|
+
cleanups2.push(fn);
|
|
74991
|
+
}
|
|
74992
|
+
function withTimeout2(p, ms, fallback) {
|
|
74993
|
+
let timer;
|
|
74994
|
+
const timeout = new Promise((r) => {
|
|
74995
|
+
timer = setTimeout(() => r(fallback), ms);
|
|
74996
|
+
});
|
|
74997
|
+
return Promise.race([p, timeout]).finally(() => clearTimeout(timer));
|
|
74998
|
+
}
|
|
74999
|
+
function cancelActiveRun2(timeoutMs = CANCEL_TIMEOUT_MS2) {
|
|
75000
|
+
if (cancelling2)
|
|
75001
|
+
return cancelling2;
|
|
75002
|
+
const pending = activeRun2;
|
|
75003
|
+
if (!pending)
|
|
75004
|
+
return Promise.resolve({ ok: false, todoId: null });
|
|
75005
|
+
const res = { ok: false };
|
|
75006
|
+
const attempt = (async () => {
|
|
75007
|
+
const run2 = await pending;
|
|
75008
|
+
res.todoId = run2?.todoId ?? null;
|
|
75009
|
+
if (!run2)
|
|
75010
|
+
return res;
|
|
75011
|
+
if (!run2.ws.connected && !await run2.ws.connect())
|
|
75012
|
+
return res;
|
|
75013
|
+
res.ok = await run2.ws.sendInterrupt(run2.projectId, run2.todoId);
|
|
75014
|
+
return res;
|
|
75015
|
+
})().catch(() => res);
|
|
75016
|
+
cancelling2 = withTimeout2(attempt, timeoutMs, res).finally(() => {
|
|
75017
|
+
if (activeRun2 === pending)
|
|
75018
|
+
activeRun2 = null;
|
|
75019
|
+
cancelling2 = null;
|
|
75020
|
+
});
|
|
75021
|
+
return cancelling2;
|
|
75022
|
+
}
|
|
75023
|
+
function shutdown2(code, reason) {
|
|
75024
|
+
return exiting2 ??= (async () => {
|
|
75025
|
+
if (reason)
|
|
75026
|
+
process.stderr.write(`
|
|
75027
|
+
${reason}
|
|
75028
|
+
`);
|
|
75029
|
+
if (activeRun2) {
|
|
75030
|
+
const { ok, todoId } = await cancelActiveRun2();
|
|
75031
|
+
if (ok)
|
|
75032
|
+
process.stderr.write(`Stopped todo ${todoId}
|
|
75033
|
+
`);
|
|
75034
|
+
else if (todoId)
|
|
75035
|
+
process.stderr.write(`Warning: could not stop todo ${todoId} (backend unreachable)
|
|
75036
|
+
`);
|
|
75037
|
+
else if (todoId === undefined)
|
|
75038
|
+
process.stderr.write(`Warning: could not stop the todo being created (timed out)
|
|
75039
|
+
`);
|
|
75040
|
+
}
|
|
75041
|
+
for (const fn of cleanups2.splice(0)) {
|
|
75042
|
+
try {
|
|
75043
|
+
fn();
|
|
75044
|
+
} catch {}
|
|
75045
|
+
}
|
|
75046
|
+
process.exit(code);
|
|
75047
|
+
})();
|
|
75048
|
+
}
|
|
75049
|
+
function finish(code) {
|
|
75050
|
+
if (exiting2)
|
|
75051
|
+
return;
|
|
75052
|
+
process.exitCode = code;
|
|
75053
|
+
process.stdout.end(() => {
|
|
75054
|
+
if (!exiting2)
|
|
75055
|
+
process.exit(code);
|
|
75056
|
+
});
|
|
75057
|
+
}
|
|
75058
|
+
function installSignalHandlers() {
|
|
75059
|
+
process.on("SIGINT", () => void shutdown2(130, "Cancelled by user (Ctrl+C)"));
|
|
75060
|
+
process.on("SIGTERM", () => void shutdown2(143, "Terminated (SIGTERM)"));
|
|
75061
|
+
process.on("SIGHUP", () => void shutdown2(129, "Hangup (SIGHUP)"));
|
|
75062
|
+
}
|
|
75063
|
+
|
|
72901
75064
|
// src/index.ts
|
|
72902
75065
|
try {
|
|
72903
75066
|
const pkgPath = path4.resolve(fileURLToPath(import.meta.url), "../../package.json");
|
|
@@ -72973,7 +75136,7 @@ async function interactiveLoop(ws, api2, todoId, projectId, agent2, json2, autoA
|
|
|
72973
75136
|
cfg.addToHistory(input2);
|
|
72974
75137
|
process.stderr.write("\u2500".repeat(40) + `
|
|
72975
75138
|
`);
|
|
72976
|
-
await api2
|
|
75139
|
+
await appendTracked(api2, ws, projectId, input2, agent2, todoId);
|
|
72977
75140
|
await watchTodo(ws, todoId, projectId, {
|
|
72978
75141
|
json: json2,
|
|
72979
75142
|
autoApprove,
|
|
@@ -72984,6 +75147,11 @@ async function interactiveLoop(ws, api2, todoId, projectId, agent2, json2, autoA
|
|
|
72984
75147
|
}
|
|
72985
75148
|
}
|
|
72986
75149
|
}
|
|
75150
|
+
async function appendTracked(api2, ws, projectId, text, agent2, todoId) {
|
|
75151
|
+
const sent = api2.addMessage(projectId, text, agent2, todoId).then(() => todoId);
|
|
75152
|
+
trackStart(ws, projectId, sent);
|
|
75153
|
+
await sent;
|
|
75154
|
+
}
|
|
72987
75155
|
async function linkToSpawningBlock(api2, todoId) {
|
|
72988
75156
|
const { TODOFORAI_TODO_ID: pTodo, TODOFORAI_MESSAGE_ID: pMsg, TODOFORAI_BLOCK_ID: pBlock } = process.env;
|
|
72989
75157
|
if (!pTodo || !pMsg || !pBlock || pTodo === todoId)
|
|
@@ -72992,12 +75160,7 @@ async function linkToSpawningBlock(api2, todoId) {
|
|
|
72992
75160
|
`));
|
|
72993
75161
|
}
|
|
72994
75162
|
async function main() {
|
|
72995
|
-
|
|
72996
|
-
process.stderr.write(`
|
|
72997
|
-
Cancelled by user (Ctrl+C)
|
|
72998
|
-
`);
|
|
72999
|
-
process.exit(130);
|
|
73000
|
-
});
|
|
75163
|
+
installSignalHandlers();
|
|
73001
75164
|
const { values: args, positionals } = parseCliArgs();
|
|
73002
75165
|
const groupTag = args.group === undefined ? getEnv("GROUP_ID") : String(args.group);
|
|
73003
75166
|
const groupName = args["group-name"] === undefined ? undefined : String(args["group-name"]);
|
|
@@ -73042,11 +75205,19 @@ Cancelled by user (Ctrl+C)
|
|
|
73042
75205
|
printDeviceHelp();
|
|
73043
75206
|
process.exit(0);
|
|
73044
75207
|
}
|
|
75208
|
+
if (positionals[0] === "inbox" && args.help) {
|
|
75209
|
+
printInboxHelp();
|
|
75210
|
+
process.exit(0);
|
|
75211
|
+
}
|
|
75212
|
+
if (positionals[0] === "notify" && args.help) {
|
|
75213
|
+
printNotifyHelp();
|
|
75214
|
+
process.exit(0);
|
|
75215
|
+
}
|
|
73045
75216
|
if ((positionals[0] === "list" || positionals[0] === "ls") && args.help) {
|
|
73046
75217
|
printListTodosHelp();
|
|
73047
75218
|
process.exit(0);
|
|
73048
75219
|
}
|
|
73049
|
-
if (args.help && !["list", "ls", "agent", "todo", "project", "brand", "device"].includes(positionals[0])) {
|
|
75220
|
+
if (args.help && !["list", "ls", "agent", "todo", "project", "brand", "device", "inbox", "notify"].includes(positionals[0])) {
|
|
73050
75221
|
printUsage();
|
|
73051
75222
|
process.exit(0);
|
|
73052
75223
|
}
|
|
@@ -73177,6 +75348,14 @@ Cancelled by user (Ctrl+C)
|
|
|
73177
75348
|
await deviceCommand(api2, positionals, args);
|
|
73178
75349
|
return;
|
|
73179
75350
|
}
|
|
75351
|
+
if (positionals[0] === "inbox") {
|
|
75352
|
+
await inboxCommand(api2, positionals, args);
|
|
75353
|
+
return;
|
|
75354
|
+
}
|
|
75355
|
+
if (positionals[0] === "notify") {
|
|
75356
|
+
await notifyCommand(api2, positionals, args);
|
|
75357
|
+
return;
|
|
75358
|
+
}
|
|
73180
75359
|
if (positionals[0] === "delete") {
|
|
73181
75360
|
const todoId = positionals[1];
|
|
73182
75361
|
if (!todoId) {
|
|
@@ -73533,6 +75712,7 @@ Cancelled by user (Ctrl+C)
|
|
|
73533
75712
|
}
|
|
73534
75713
|
if (!args["no-watch"]) {
|
|
73535
75714
|
const ws2 = new FrontendWebSocket(apiUrl, apiKey);
|
|
75715
|
+
setActiveRun2({ ws: ws2, projectId: projectId2, todoId });
|
|
73536
75716
|
await ws2.connect();
|
|
73537
75717
|
const autoApprove = !!args["dangerously-skip-permissions"];
|
|
73538
75718
|
let agent3 = todo2.agentSettings || { id: todo2.agentSettingsId };
|
|
@@ -73543,7 +75723,8 @@ Cancelled by user (Ctrl+C)
|
|
|
73543
75723
|
await watchTodo(ws2, todoId, projectId2, {
|
|
73544
75724
|
json: !!args.json,
|
|
73545
75725
|
autoApprove,
|
|
73546
|
-
agentSettings: agent3
|
|
75726
|
+
agentSettings: agent3,
|
|
75727
|
+
exitOnInterrupt: !!args["non-interactive"]
|
|
73547
75728
|
});
|
|
73548
75729
|
if (!args["non-interactive"]) {
|
|
73549
75730
|
process.stderr.write(`
|
|
@@ -73592,9 +75773,9 @@ Resumed: ${CYAN2}${getFrontendUrl(apiUrl, todoId)}${RESET2}
|
|
|
73592
75773
|
const followUp = promptArg(positionals) ?? (process.stdin.isTTY ? "" : await readStdin());
|
|
73593
75774
|
if (followUp) {
|
|
73594
75775
|
cfg.addToHistory(followUp);
|
|
73595
|
-
await api2
|
|
75776
|
+
await appendTracked(api2, ws2, projectId2, followUp, agent3, todoId);
|
|
73596
75777
|
await linkToSpawningBlock(api2, todoId);
|
|
73597
|
-
await watchTodo(ws2, todoId, projectId2, { json: !!args.json, autoApprove, agentSettings: agent3 });
|
|
75778
|
+
await watchTodo(ws2, todoId, projectId2, { json: !!args.json, autoApprove, agentSettings: agent3, exitOnInterrupt: !!args["non-interactive"] });
|
|
73598
75779
|
}
|
|
73599
75780
|
if (!args["non-interactive"]) {
|
|
73600
75781
|
await interactiveLoop(ws2, api2, todoId, projectId2, agent3, !!args.json, autoApprove, cfg);
|
|
@@ -73613,9 +75794,15 @@ Resumed: ${CYAN2}${getFrontendUrl(apiUrl, todoId)}${RESET2}
|
|
|
73613
75794
|
const bridgeReady = !args["no-bridge"] && !args["no-watch"] && !args.isolated ? ensureBridgeRunning2(apiUrl, apiKey) : null;
|
|
73614
75795
|
const isolatedTodoId = args.isolated ? randomUUID3() : undefined;
|
|
73615
75796
|
const mayfly = isolatedTodoId ? await spawnMayflyBridge(apiUrl, isolatedTodoId, realpathSync2(resolve4(args.path || ".")), { debug: !!args.debug, apiKey }) : null;
|
|
73616
|
-
if (mayfly)
|
|
75797
|
+
if (mayfly) {
|
|
73617
75798
|
process.stderr.write(`${DIM2}Isolated bridge:${RESET2} ${CYAN2}mayfly-${isolatedTodoId}${RESET2}
|
|
73618
75799
|
`);
|
|
75800
|
+
onShutdown(mayfly.stop);
|
|
75801
|
+
mayfly.child.on("exit", (code, sig) => {
|
|
75802
|
+
if (!mayfly.stopped())
|
|
75803
|
+
shutdown2(1, `${RED3}Error: isolated bridge died (${sig ?? `code ${code}`}) \u2014 stopping todo${RESET2}`);
|
|
75804
|
+
});
|
|
75805
|
+
}
|
|
73619
75806
|
let preMatchedAgent = null;
|
|
73620
75807
|
let agents = null;
|
|
73621
75808
|
if (args.agent) {
|
|
@@ -73719,15 +75906,16 @@ Resumed: ${CYAN2}${getFrontendUrl(apiUrl, todoId)}${RESET2}
|
|
|
73719
75906
|
agent2 = { ...agent2, permissions: { ...perms, allow: [...perms.allow || [], "*:*"] } };
|
|
73720
75907
|
}
|
|
73721
75908
|
cfg.addToHistory(content);
|
|
75909
|
+
const created = api2.addMessage(projectId, content, agent2, isolatedTodoId, undefined, undefined, groupTag || undefined, groupName);
|
|
75910
|
+
if (ws)
|
|
75911
|
+
trackStart(ws, projectId, created.then((t) => t.id));
|
|
73722
75912
|
let todo;
|
|
73723
75913
|
try {
|
|
73724
|
-
todo = await
|
|
75914
|
+
todo = await created;
|
|
73725
75915
|
} catch (e) {
|
|
73726
75916
|
if (!args.project && !envProjectId && cfgScope.data.default_project_id === projectId && /failed: 403/.test(e.message || "")) {
|
|
73727
75917
|
cfgScope.clearDefaultProject();
|
|
73728
|
-
|
|
73729
|
-
`);
|
|
73730
|
-
process.exit(1);
|
|
75918
|
+
throw new Error(`Not authorized for cached default project ${projectName} (${projectId}) \u2014 cleared it. Re-run to pick a project.`);
|
|
73731
75919
|
}
|
|
73732
75920
|
throw e;
|
|
73733
75921
|
}
|
|
@@ -73748,7 +75936,8 @@ Resumed: ${CYAN2}${getFrontendUrl(apiUrl, todoId)}${RESET2}
|
|
|
73748
75936
|
await watchTodo(ws, actualTodoId, projectId, {
|
|
73749
75937
|
json: !!args.json,
|
|
73750
75938
|
autoApprove,
|
|
73751
|
-
agentSettings: agent2
|
|
75939
|
+
agentSettings: agent2,
|
|
75940
|
+
exitOnInterrupt: !!args["non-interactive"]
|
|
73752
75941
|
});
|
|
73753
75942
|
if (!args["non-interactive"]) {
|
|
73754
75943
|
process.stderr.write(`
|
|
@@ -73760,10 +75949,6 @@ ${"\u2500".repeat(40)}
|
|
|
73760
75949
|
}
|
|
73761
75950
|
mayfly?.stop();
|
|
73762
75951
|
}
|
|
73763
|
-
var finish = (code) => {
|
|
73764
|
-
process.exitCode = code;
|
|
73765
|
-
process.stdout.end(() => process.exit(code));
|
|
73766
|
-
};
|
|
73767
75952
|
main().then(() => finish(process.exitCode ?? 0)).catch((e) => {
|
|
73768
75953
|
process.stderr.write(`Error: ${e.message}
|
|
73769
75954
|
`);
|