@todoforai/tfa-review 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +50 -63
  2. package/package.json +3 -3
package/dist/cli.js CHANGED
@@ -2879,11 +2879,7 @@ var require_websocket_server = __commonJS((exports, module) => {
2879
2879
  // src/cli.ts
2880
2880
  import { parseArgs } from "node:util";
2881
2881
  import { execFileSync } from "node:child_process";
2882
-
2883
- // ../tfa-subagent/dist/index.js
2884
- import { homedir, platform } from "os";
2885
- import { readFileSync } from "fs";
2886
- import { join } from "path";
2882
+ import { readFileSync } from "node:fs";
2887
2883
 
2888
2884
  // ../node_modules/ws/wrapper.mjs
2889
2885
  var import_stream = __toESM(require_stream(), 1);
@@ -2909,45 +2905,23 @@ function normalizeApiUrl(url) {
2909
2905
  return `https://${url}`;
2910
2906
  return url.replace(/\/+$/, "");
2911
2907
  }
2912
- function credentialsPath() {
2913
- const home = homedir();
2914
- if (platform() === "win32")
2915
- return join(home, "AppData", "Roaming", "todoforai", "credentials.json");
2916
- if (platform() === "darwin")
2917
- return join(home, "Library", "Application Support", "todoforai", "credentials.json");
2918
- const xdg = process.env.XDG_CONFIG_HOME;
2919
- return xdg ? join(xdg, "todoforai", "credentials.json") : join(home, ".config", "todoforai", "credentials.json");
2920
- }
2921
- var _credsCache;
2922
- function loadCredentials() {
2923
- if (_credsCache !== undefined)
2924
- return _credsCache;
2925
- try {
2926
- _credsCache = JSON.parse(readFileSync(credentialsPath(), "utf8"));
2927
- } catch {
2928
- _credsCache = null;
2929
- }
2930
- return _credsCache;
2931
- }
2932
2908
  function resolveAuth(opts = {}) {
2933
- const creds = loadCredentials();
2934
- const credsApiUrl = creds?.backendHost ? normalizeApiUrl(creds.backendHost) : "";
2935
- const apiUrl = normalizeApiUrl(opts.apiUrl || getEnv("TODOFORAI_API_URL") || credsApiUrl || DEFAULT_API_URL);
2936
- const apiKey = opts.apiKey || getEnv("TODOFORAI_API_KEY") || getEnv("TODOFORAI_API_TOKEN") || creds?.apiKey || "";
2909
+ const apiUrl = normalizeApiUrl(opts.apiUrl || getEnv("TODOFORAI_API_URL") || DEFAULT_API_URL);
2910
+ const apiToken = opts.apiToken || getEnv("TODOFORAI_API_TOKEN");
2937
2911
  const agentSettingsId = opts.agentSettingsId || getEnv("TODOFORAI_AGENT_SETTINGS_ID");
2938
- if (!apiKey)
2939
- throw new Error("no API key (run `todoforai-bridge login`, or set TODOFORAI_API_KEY, or pass --api-key)");
2912
+ if (!apiToken)
2913
+ throw new Error("TODOFORAI_API_TOKEN not set (run `todoforai-bridge` to start the bridge, or pass --api-token)");
2940
2914
  if (!agentSettingsId)
2941
2915
  throw new Error("agent settings id required (set TODOFORAI_AGENT_SETTINGS_ID or pass --agent <id>)");
2942
- const apiBase = apiKey.startsWith("dst_") ? "/dst/v1" : "/api/v1";
2943
- return { apiUrl, apiKey, agentSettingsId, apiBase };
2916
+ const apiBase = apiToken.startsWith("dst_") ? "/dst/v1" : "/api/v1";
2917
+ return { apiUrl, apiToken, agentSettingsId, apiBase };
2944
2918
  }
2945
- async function api(path, init = {}, apiUrl, apiKey, tabId) {
2919
+ async function api(path, init = {}, apiUrl, apiToken, tabId) {
2946
2920
  const res = await fetch(`${apiUrl}${path}`, {
2947
2921
  ...init,
2948
2922
  headers: {
2949
2923
  "content-type": "application/json",
2950
- "x-api-key": apiKey,
2924
+ "x-api-key": apiToken,
2951
2925
  ...tabId ? { "x-tab-id": tabId } : {},
2952
2926
  ...init.headers ?? {}
2953
2927
  }
@@ -2999,7 +2973,7 @@ var TERMINAL = new Set([
2999
2973
  ]);
3000
2974
  async function openSocket(opts) {
3001
2975
  const wsUrl = opts.apiUrl.replace(/^http/, "ws") + "/ws/v1/frontend?tabId=" + encodeURIComponent(opts.tabId);
3002
- const ws = new wrapper_default(wsUrl, [opts.apiKey]);
2976
+ const ws = new wrapper_default(wsUrl, [opts.apiToken]);
3003
2977
  ws.on("error", (err) => process.stderr.write(`[ws error] ${err.message}
3004
2978
  `));
3005
2979
  await new Promise((ok, fail) => {
@@ -3023,7 +2997,6 @@ async function streamToTerminal(ws, opts = {}) {
3023
2997
  return new Promise((resolve) => {
3024
2998
  let exitCode = 0;
3025
2999
  let result = "";
3026
- let inText = false;
3027
3000
  const finish = (code) => {
3028
3001
  exitCode = code;
3029
3002
  ws.close();
@@ -3040,18 +3013,14 @@ async function streamToTerminal(ws, opts = {}) {
3040
3013
  if (!t)
3041
3014
  return;
3042
3015
  if (t === "block:message" && typeof ev.payload?.content === "string") {
3043
- if (inText)
3044
- result += ev.payload.content;
3016
+ result += ev.payload.content;
3045
3017
  if (streamStdout)
3046
3018
  process.stdout.write(ev.payload.content);
3047
3019
  return;
3048
3020
  }
3049
3021
  if (t === "block:start_universal" && ev.payload?.block_type) {
3050
3022
  const bt = ev.payload.block_type;
3051
- inText = bt === "TEXT";
3052
- if (inText)
3053
- result = "";
3054
- else if (streamStdout && bt !== "REASON")
3023
+ if (bt !== "TEXT" && bt !== "REASON")
3055
3024
  process.stderr.write(`
3056
3025
  → ${bt}
3057
3026
  `);
@@ -3074,9 +3043,9 @@ async function streamToTerminal(ws, opts = {}) {
3074
3043
  });
3075
3044
  }
3076
3045
  async function runTfa(spec) {
3077
- const { apiUrl, apiKey, agentSettingsId, apiBase } = resolveAuth({
3046
+ const { apiUrl, apiToken, agentSettingsId, apiBase } = resolveAuth({
3078
3047
  apiUrl: spec.apiUrl,
3079
- apiKey: spec.apiKey,
3048
+ apiToken: spec.apiToken,
3080
3049
  agentSettingsId: spec.agentSettingsId
3081
3050
  });
3082
3051
  const allowed = new Set(spec.allowedTools);
@@ -3088,7 +3057,7 @@ async function runTfa(spec) {
3088
3057
  }
3089
3058
  const allowTools = requested.length ? requested : spec.allowedTools;
3090
3059
  const tabId = crypto.randomUUID();
3091
- const settings = await api(`${apiBase}/agents/${agentSettingsId}`, {}, apiUrl, apiKey);
3060
+ const settings = await api(`${apiBase}/agents/${agentSettingsId}`, {}, apiUrl, apiToken);
3092
3061
  const patched = patchSettings(settings, {
3093
3062
  systemMessage: spec.systemMessage,
3094
3063
  allowTools,
@@ -3097,22 +3066,22 @@ async function runTfa(spec) {
3097
3066
  });
3098
3067
  let projectId = spec.projectId || "";
3099
3068
  if (!projectId) {
3100
- const projects = await api(`${apiBase}/projects`, {}, apiUrl, apiKey);
3069
+ const projects = await api(`${apiBase}/projects`, {}, apiUrl, apiToken);
3101
3070
  if (!projects?.length)
3102
3071
  throw new Error("no project found. Pass --project <id>.");
3103
3072
  projectId = projects[0].project?.id ?? projects[0].id;
3104
3073
  }
3105
- const ws = await openSocket({ apiUrl, apiKey, tabId });
3074
+ const ws = await openSocket({ apiUrl, apiToken, tabId });
3106
3075
  const visible = spec.visible === true;
3107
3076
  const todoRes = await api(`${apiBase}/projects/${projectId}/todos`, {
3108
3077
  method: "POST",
3109
3078
  body: JSON.stringify({ content: spec.content, agentSettings: patched, visible })
3110
- }, apiUrl, apiKey, tabId);
3079
+ }, apiUrl, apiToken, tabId);
3111
3080
  const todoId = todoRes.id;
3112
3081
  await api(`${apiBase}/todos/${todoId}/subscribe`, {
3113
3082
  method: "POST",
3114
3083
  body: JSON.stringify({ todoId })
3115
- }, apiUrl, apiKey, tabId);
3084
+ }, apiUrl, apiToken, tabId);
3116
3085
  process.stderr.write(`[${spec.binName}] todo=${todoId}${visible ? "" : " (hidden)"}
3117
3086
  `);
3118
3087
  const { exitCode, result } = await streamToTerminal(ws, { streamStdout: visible });
@@ -3136,17 +3105,34 @@ Use git diff, git status, read files, and search to understand what was done. Th
3136
3105
  IMPORTANT: You are read-only. Do NOT modify, create, or delete any files. Do NOT run commands that change state. Only read, inspect, and report findings.
3137
3106
  If a tool fails 3 times, stop retrying and report that the tools are faulty.`;
3138
3107
  var ALLOWED_TOOLS = ["read", "grep", "bash"];
3108
+ function readStdinDiff() {
3109
+ if (process.stdin.isTTY)
3110
+ return "";
3111
+ try {
3112
+ return readFileSync(0, "utf-8");
3113
+ } catch {
3114
+ return "";
3115
+ }
3116
+ }
3139
3117
  function captureGitDiff(repo, against) {
3118
+ try {
3119
+ execFileSync("git", ["-C", repo, "rev-parse", "--is-inside-work-tree"], { stdio: "ignore" });
3120
+ } catch {
3121
+ console.error(`Not a git repository: ${repo}`);
3122
+ process.exit(1);
3123
+ }
3140
3124
  const args = against === "WORKING" ? ["-C", repo, "diff", "HEAD"] : ["-C", repo, "diff", `${against}...HEAD`];
3141
3125
  try {
3142
3126
  return execFileSync("git", args, { encoding: "utf-8", maxBuffer: 50 * 1024 * 1024 });
3143
3127
  } catch (e) {
3144
- console.error(`git diff failed: ${e?.message ?? e}`);
3128
+ const stderr = (e?.stderr?.toString?.() || "").split(`
3129
+ `)[0] || e?.message || String(e);
3130
+ console.error(`git diff failed (${against} in ${repo}): ${stderr}`);
3145
3131
  process.exit(1);
3146
3132
  }
3147
3133
  }
3148
3134
  function printHelp() {
3149
- console.log(`tfa-review — review a git diff as a real TODO; stream output to terminal.
3135
+ console.log(`tfa-review — review a git diff via a sub-agent TODO.
3150
3136
 
3151
3137
  Usage:
3152
3138
  tfa-review [options] <goal>
@@ -3159,11 +3145,12 @@ Options:
3159
3145
  -a, --agent <id> Agent settings ID (default: $TODOFORAI_AGENT_SETTINGS_ID)
3160
3146
  -p, --project <id> Project ID (default: first project the user owns)
3161
3147
  --tools <a,b,c> Restrict to a subset of ${ALLOWED_TOOLS.join(",")} (comma-separated)
3148
+ --visible List the TODO in the dashboard project view (otherwise only its id is logged).
3162
3149
  -h, --help Show this help
3163
3150
  -v, --version Show version
3164
3151
 
3165
3152
  Env (auto-injected by edge shell):
3166
- TODOFORAI_API_URL, TODOFORAI_API_KEY, TODOFORAI_AGENT_SETTINGS_ID
3153
+ TODOFORAI_API_URL, TODOFORAI_API_TOKEN, TODOFORAI_AGENT_SETTINGS_ID
3167
3154
  `);
3168
3155
  }
3169
3156
  async function main() {
@@ -3176,6 +3163,7 @@ async function main() {
3176
3163
  agent: { type: "string", short: "a" },
3177
3164
  project: { type: "string", short: "p" },
3178
3165
  tools: { type: "string" },
3166
+ visible: { type: "boolean", default: false },
3179
3167
  help: { type: "boolean", short: "h", default: false },
3180
3168
  version: { type: "boolean", short: "v", default: false }
3181
3169
  },
@@ -3190,30 +3178,28 @@ async function main() {
3190
3178
  console.log("tfa-review 0.1.0");
3191
3179
  return;
3192
3180
  }
3193
- const goal = positionals.join(" ").trim();
3194
- if (!goal) {
3195
- console.error("Error: missing goal. Run `tfa-review --help`.");
3196
- process.exit(2);
3197
- }
3198
3181
  const repo = values.repo || process.cwd();
3199
3182
  const against = values.against || "WORKING";
3200
- const diff = captureGitDiff(repo, against);
3183
+ const stdinDiff = readStdinDiff();
3184
+ const diff = stdinDiff.trim() ? stdinDiff : captureGitDiff(repo, against);
3185
+ const goal = positionals.join(" ").trim() || "Review this diff. Assess correctness, flag issues, suggest simpler approaches.";
3201
3186
  if (!diff.trim()) {
3202
3187
  console.error(`No changes between ${against} and HEAD in ${repo}.`);
3203
3188
  process.exit(1);
3204
3189
  }
3190
+ const diffLabel = stdinDiff.trim() ? "stdin" : `${against}...HEAD`;
3205
3191
  const content = `# Goal
3206
3192
  ${goal}
3207
3193
 
3208
3194
  # Repo
3209
3195
  ${repo}
3210
3196
 
3211
- # Diff (${against}...HEAD)
3197
+ # Diff (${diffLabel})
3212
3198
  \`\`\`diff
3213
3199
  ${diff}
3214
3200
  \`\`\``;
3215
3201
  const requestedTools = values.tools ? String(values.tools).split(",").map((s) => s.trim()).filter(Boolean) : [];
3216
- const exit = await runTfa({
3202
+ const { exitCode } = await runTfa({
3217
3203
  binName: "tfa-review",
3218
3204
  systemMessage: REVIEW_SYS_PROMPT,
3219
3205
  allowedTools: ALLOWED_TOOLS,
@@ -3222,9 +3208,10 @@ ${diff}
3222
3208
  model: values.model,
3223
3209
  agentSettingsId: values.agent,
3224
3210
  projectId: values.project,
3225
- requestedTools
3211
+ requestedTools,
3212
+ visible: values.visible === true
3226
3213
  });
3227
- process.exit(exit);
3214
+ process.exit(exitCode);
3228
3215
  }
3229
3216
  main().catch((e) => {
3230
3217
  console.error(`Error: ${e?.message ?? e}`);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@todoforai/tfa-review",
3
- "version": "0.1.0",
4
- "description": "Lightweight TODO for AI review — captures a git diff, creates a real TODO with patched permissions, and streams blocks to your terminal.",
3
+ "version": "0.1.2",
4
+ "description": "Review a git diff via a sub-agent TODO. Not listed in the dashboard unless --visible.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "tfa-review": "./dist/cli.js"
@@ -11,7 +11,7 @@
11
11
  "dev": "bun run src/cli.ts"
12
12
  },
13
13
  "devDependencies": {
14
- "@todoforai/tfa-subagent": "workspace:*",
14
+ "@todoforai/tfa-subagent": "^0.1.1",
15
15
  "@types/node": "^20.11.0"
16
16
  },
17
17
  "keywords": ["cli", "ai", "review", "todoforai"],