@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.
- package/dist/cli.js +50 -63
- 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
|
|
2934
|
-
const
|
|
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 (!
|
|
2939
|
-
throw new Error("
|
|
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 =
|
|
2943
|
-
return { apiUrl,
|
|
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,
|
|
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":
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
3046
|
+
const { apiUrl, apiToken, agentSettingsId, apiBase } = resolveAuth({
|
|
3078
3047
|
apiUrl: spec.apiUrl,
|
|
3079
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
|
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 (${
|
|
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
|
|
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(
|
|
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.
|
|
4
|
-
"description": "
|
|
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": "
|
|
14
|
+
"@todoforai/tfa-subagent": "^0.1.1",
|
|
15
15
|
"@types/node": "^20.11.0"
|
|
16
16
|
},
|
|
17
17
|
"keywords": ["cli", "ai", "review", "todoforai"],
|