polymath-agent 0.1.0 → 0.2.0
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 +6 -2
- package/dist/cli.js +836 -83
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -93,8 +93,12 @@ poly usage # cost by date + model
|
|
|
93
93
|
| `poly recommend <goal>` | Pre-run recommendation: cheapest / best-value / best-quality model combos + savings. |
|
|
94
94
|
| `poly models` | Browse the catalog with pricing, tier, tool support. Filters: `--tier`, `--tools`, `--search`. |
|
|
95
95
|
| `poly usage` | Recorded usage & cost grouped by **date + model**. `--today`, `--since`, `--sync`. |
|
|
96
|
-
| `poly
|
|
97
|
-
| `poly
|
|
96
|
+
| `poly analyze` | **Which approach reaches the goal with the fewest tokens** — best model per task type, objective × achievement, usage per command. |
|
|
97
|
+
| `poly sync` | Push the analytics ledger to Firebase ([Data Connect SQL](dataconnect/) and/or Firestore). |
|
|
98
|
+
| `poly config show\|set\|firestore\|dataconnect` | View/change settings. |
|
|
99
|
+
|
|
100
|
+
After each `poly run`, rate the result 0–9 (one keypress) — your goal-achievement
|
|
101
|
+
rating joins the auto score (completed/planned steps) to power `poly analyze`.
|
|
98
102
|
|
|
99
103
|
### Routing objectives
|
|
100
104
|
|
package/dist/cli.js
CHANGED
|
@@ -44,6 +44,11 @@ var DEFAULT_CONFIG = {
|
|
|
44
44
|
enabled: false,
|
|
45
45
|
projectId: "mathology-b8e3d",
|
|
46
46
|
collection: "polymath_usage"
|
|
47
|
+
},
|
|
48
|
+
dataconnect: {
|
|
49
|
+
enabled: false,
|
|
50
|
+
location: "us-east4",
|
|
51
|
+
serviceId: "polymath"
|
|
47
52
|
}
|
|
48
53
|
};
|
|
49
54
|
function loadConfig() {
|
|
@@ -54,7 +59,8 @@ function loadConfig() {
|
|
|
54
59
|
return {
|
|
55
60
|
...DEFAULT_CONFIG,
|
|
56
61
|
...raw,
|
|
57
|
-
firestore: { ...DEFAULT_CONFIG.firestore, ...raw.firestore ?? {} }
|
|
62
|
+
firestore: { ...DEFAULT_CONFIG.firestore, ...raw.firestore ?? {} },
|
|
63
|
+
dataconnect: { ...DEFAULT_CONFIG.dataconnect, ...raw.dataconnect ?? {} }
|
|
58
64
|
};
|
|
59
65
|
} catch {
|
|
60
66
|
return { ...DEFAULT_CONFIG };
|
|
@@ -560,7 +566,7 @@ function heuristicPlan(goal) {
|
|
|
560
566
|
];
|
|
561
567
|
return { goal, steps };
|
|
562
568
|
}
|
|
563
|
-
async function planRequest(goal, client2, planModel) {
|
|
569
|
+
async function planRequest(goal, client2, planModel, onUsage) {
|
|
564
570
|
const result = await client2.complete(
|
|
565
571
|
{
|
|
566
572
|
model: planModel.id,
|
|
@@ -573,6 +579,7 @@ async function planRequest(goal, client2, planModel) {
|
|
|
573
579
|
},
|
|
574
580
|
planModel.pricing
|
|
575
581
|
);
|
|
582
|
+
onUsage?.(result);
|
|
576
583
|
const parsed = extractPlan(result.content);
|
|
577
584
|
if (!parsed) return heuristicPlan(goal);
|
|
578
585
|
return { goal, steps: parsed };
|
|
@@ -935,14 +942,78 @@ function getDb() {
|
|
|
935
942
|
);
|
|
936
943
|
CREATE INDEX IF NOT EXISTS idx_usage_date ON usage_log(date);
|
|
937
944
|
CREATE INDEX IF NOT EXISTS idx_usage_model ON usage_log(model);
|
|
945
|
+
|
|
946
|
+
-- One row per agent session (a \`poly run\`): goal + outcome + achievement scores.
|
|
947
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
948
|
+
id TEXT PRIMARY KEY,
|
|
949
|
+
ts INTEGER NOT NULL,
|
|
950
|
+
date TEXT NOT NULL,
|
|
951
|
+
goal TEXT NOT NULL,
|
|
952
|
+
command TEXT NOT NULL DEFAULT 'run',
|
|
953
|
+
objective TEXT NOT NULL,
|
|
954
|
+
planned_steps INTEGER NOT NULL DEFAULT 0,
|
|
955
|
+
completed_steps INTEGER NOT NULL DEFAULT 0,
|
|
956
|
+
failed_steps INTEGER NOT NULL DEFAULT 0,
|
|
957
|
+
auto_score REAL, -- 0..1 = completed/planned (agent-computed)
|
|
958
|
+
user_score INTEGER, -- 0..9 user-rated goal achievement (nullable)
|
|
959
|
+
prompt_tokens INTEGER NOT NULL DEFAULT 0,
|
|
960
|
+
completion_tokens INTEGER NOT NULL DEFAULT 0,
|
|
961
|
+
cost_usd REAL NOT NULL DEFAULT 0,
|
|
962
|
+
duration_ms INTEGER NOT NULL DEFAULT 0,
|
|
963
|
+
synced INTEGER NOT NULL DEFAULT 0
|
|
964
|
+
);
|
|
965
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_date ON sessions(date);
|
|
966
|
+
|
|
967
|
+
-- One row per executed plan step: which model, how many round-trips, how it ended.
|
|
968
|
+
CREATE TABLE IF NOT EXISTS step_runs (
|
|
969
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
970
|
+
session_id TEXT NOT NULL,
|
|
971
|
+
step_no INTEGER NOT NULL,
|
|
972
|
+
task_type TEXT NOT NULL,
|
|
973
|
+
skill TEXT NOT NULL,
|
|
974
|
+
model TEXT NOT NULL,
|
|
975
|
+
provider TEXT NOT NULL,
|
|
976
|
+
iterations INTEGER NOT NULL, -- LLM round-trips used for this step
|
|
977
|
+
tool_calls INTEGER NOT NULL,
|
|
978
|
+
prompt_tokens INTEGER NOT NULL,
|
|
979
|
+
completion_tokens INTEGER NOT NULL,
|
|
980
|
+
cost_usd REAL NOT NULL,
|
|
981
|
+
finished_by TEXT NOT NULL, -- 'finish-tool' | 'text' | 'max-iters' | 'error'
|
|
982
|
+
success INTEGER NOT NULL, -- 1 = ended cleanly (finish-tool or text)
|
|
983
|
+
duration_ms INTEGER NOT NULL,
|
|
984
|
+
synced INTEGER NOT NULL DEFAULT 0
|
|
985
|
+
);
|
|
986
|
+
CREATE INDEX IF NOT EXISTS idx_steps_session ON step_runs(session_id);
|
|
987
|
+
CREATE INDEX IF NOT EXISTS idx_steps_model ON step_runs(model, task_type);
|
|
988
|
+
|
|
989
|
+
-- One row per CLI command invocation (run/recommend/...): tokens spent per command.
|
|
990
|
+
CREATE TABLE IF NOT EXISTS command_runs (
|
|
991
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
992
|
+
session_id TEXT,
|
|
993
|
+
ts INTEGER NOT NULL,
|
|
994
|
+
date TEXT NOT NULL,
|
|
995
|
+
command TEXT NOT NULL,
|
|
996
|
+
args TEXT,
|
|
997
|
+
objective TEXT,
|
|
998
|
+
prompt_tokens INTEGER NOT NULL DEFAULT 0,
|
|
999
|
+
completion_tokens INTEGER NOT NULL DEFAULT 0,
|
|
1000
|
+
cost_usd REAL NOT NULL DEFAULT 0,
|
|
1001
|
+
duration_ms INTEGER NOT NULL DEFAULT 0,
|
|
1002
|
+
synced INTEGER NOT NULL DEFAULT 0
|
|
1003
|
+
);
|
|
1004
|
+
CREATE INDEX IF NOT EXISTS idx_cmd_date ON command_runs(date);
|
|
938
1005
|
`);
|
|
1006
|
+
const cols = db.prepare(`PRAGMA table_info(usage_log)`).all();
|
|
1007
|
+
if (!cols.some((c2) => c2.name === "command")) {
|
|
1008
|
+
db.exec(`ALTER TABLE usage_log ADD COLUMN command TEXT NOT NULL DEFAULT 'run'`);
|
|
1009
|
+
}
|
|
939
1010
|
return db;
|
|
940
1011
|
}
|
|
941
1012
|
function recordUsage(e) {
|
|
942
1013
|
const stmt = getDb().prepare(`
|
|
943
1014
|
INSERT INTO usage_log
|
|
944
|
-
(ts, date, provider, model, task_type, prompt_tokens, completion_tokens, total_tokens, cost_usd, session_id)
|
|
945
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1015
|
+
(ts, date, provider, model, task_type, prompt_tokens, completion_tokens, total_tokens, cost_usd, session_id, command)
|
|
1016
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
946
1017
|
`);
|
|
947
1018
|
stmt.run(
|
|
948
1019
|
e.ts,
|
|
@@ -954,7 +1025,8 @@ function recordUsage(e) {
|
|
|
954
1025
|
e.completionTokens,
|
|
955
1026
|
e.totalTokens,
|
|
956
1027
|
e.costUsd,
|
|
957
|
-
e.sessionId ?? null
|
|
1028
|
+
e.sessionId ?? null,
|
|
1029
|
+
e.command ?? "run"
|
|
958
1030
|
);
|
|
959
1031
|
}
|
|
960
1032
|
function reportByDateModel(filter = {}) {
|
|
@@ -1016,7 +1088,8 @@ function unsyncedRows() {
|
|
|
1016
1088
|
completionTokens: Number(r.completion_tokens),
|
|
1017
1089
|
totalTokens: Number(r.total_tokens),
|
|
1018
1090
|
costUsd: Number(r.cost_usd),
|
|
1019
|
-
sessionId: r.session_id ? String(r.session_id) : void 0
|
|
1091
|
+
sessionId: r.session_id ? String(r.session_id) : void 0,
|
|
1092
|
+
command: r.command ? String(r.command) : "run"
|
|
1020
1093
|
}));
|
|
1021
1094
|
}
|
|
1022
1095
|
function markSynced(ids) {
|
|
@@ -1024,6 +1097,216 @@ function markSynced(ids) {
|
|
|
1024
1097
|
const stmt = getDb().prepare(`UPDATE usage_log SET synced = 1 WHERE id = ?`);
|
|
1025
1098
|
for (const id of ids) stmt.run(id);
|
|
1026
1099
|
}
|
|
1100
|
+
function startSession(s) {
|
|
1101
|
+
getDb().prepare(
|
|
1102
|
+
`INSERT OR REPLACE INTO sessions (id, ts, date, goal, command, objective, planned_steps)
|
|
1103
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`
|
|
1104
|
+
).run(s.id, s.ts, s.date, s.goal, s.command, s.objective, s.plannedSteps);
|
|
1105
|
+
}
|
|
1106
|
+
function finishSession(id, u) {
|
|
1107
|
+
getDb().prepare(
|
|
1108
|
+
`UPDATE sessions SET planned_steps=?, completed_steps=?, failed_steps=?, auto_score=?,
|
|
1109
|
+
prompt_tokens=?, completion_tokens=?, cost_usd=?, duration_ms=? WHERE id=?`
|
|
1110
|
+
).run(
|
|
1111
|
+
u.plannedSteps,
|
|
1112
|
+
u.completedSteps,
|
|
1113
|
+
u.failedSteps,
|
|
1114
|
+
u.autoScore,
|
|
1115
|
+
u.promptTokens,
|
|
1116
|
+
u.completionTokens,
|
|
1117
|
+
u.costUsd,
|
|
1118
|
+
u.durationMs,
|
|
1119
|
+
id
|
|
1120
|
+
);
|
|
1121
|
+
}
|
|
1122
|
+
function setUserScore(sessionId, score) {
|
|
1123
|
+
getDb().prepare(`UPDATE sessions SET user_score=? WHERE id=?`).run(score, sessionId);
|
|
1124
|
+
}
|
|
1125
|
+
function recordStepRun(s) {
|
|
1126
|
+
getDb().prepare(
|
|
1127
|
+
`INSERT INTO step_runs
|
|
1128
|
+
(session_id, step_no, task_type, skill, model, provider, iterations, tool_calls,
|
|
1129
|
+
prompt_tokens, completion_tokens, cost_usd, finished_by, success, duration_ms)
|
|
1130
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
1131
|
+
).run(
|
|
1132
|
+
s.sessionId,
|
|
1133
|
+
s.stepNo,
|
|
1134
|
+
s.taskType,
|
|
1135
|
+
s.skill,
|
|
1136
|
+
s.model,
|
|
1137
|
+
s.provider,
|
|
1138
|
+
s.iterations,
|
|
1139
|
+
s.toolCalls,
|
|
1140
|
+
s.promptTokens,
|
|
1141
|
+
s.completionTokens,
|
|
1142
|
+
s.costUsd,
|
|
1143
|
+
s.finishedBy,
|
|
1144
|
+
s.success ? 1 : 0,
|
|
1145
|
+
s.durationMs
|
|
1146
|
+
);
|
|
1147
|
+
}
|
|
1148
|
+
function recordCommandRun(c2) {
|
|
1149
|
+
getDb().prepare(
|
|
1150
|
+
`INSERT INTO command_runs
|
|
1151
|
+
(session_id, ts, date, command, args, objective, prompt_tokens, completion_tokens, cost_usd, duration_ms)
|
|
1152
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
1153
|
+
).run(
|
|
1154
|
+
c2.sessionId ?? null,
|
|
1155
|
+
c2.ts,
|
|
1156
|
+
c2.date,
|
|
1157
|
+
c2.command,
|
|
1158
|
+
c2.args ?? null,
|
|
1159
|
+
c2.objective ?? null,
|
|
1160
|
+
c2.promptTokens,
|
|
1161
|
+
c2.completionTokens,
|
|
1162
|
+
c2.costUsd,
|
|
1163
|
+
c2.durationMs
|
|
1164
|
+
);
|
|
1165
|
+
}
|
|
1166
|
+
function sessionUsageTotals(sessionId) {
|
|
1167
|
+
const r = getDb().prepare(
|
|
1168
|
+
`SELECT COALESCE(SUM(prompt_tokens),0) AS p, COALESCE(SUM(completion_tokens),0) AS c, COALESCE(SUM(cost_usd),0) AS cost
|
|
1169
|
+
FROM usage_log WHERE session_id = ?`
|
|
1170
|
+
).get(sessionId);
|
|
1171
|
+
return { promptTokens: Number(r?.p ?? 0), completionTokens: Number(r?.c ?? 0), costUsd: Number(r?.cost ?? 0) };
|
|
1172
|
+
}
|
|
1173
|
+
function modelTaskEfficiency(filter = {}) {
|
|
1174
|
+
const { whereSql, params } = dateWhere(filter, "s.ts");
|
|
1175
|
+
const rows = getDb().prepare(
|
|
1176
|
+
`SELECT task_type AS taskType, model,
|
|
1177
|
+
COUNT(*) AS steps,
|
|
1178
|
+
AVG(success) AS successRate,
|
|
1179
|
+
AVG(CASE WHEN success=1 THEN prompt_tokens + completion_tokens END) AS avgTokensPerSuccess,
|
|
1180
|
+
AVG(CASE WHEN success=1 THEN cost_usd END) AS avgCostPerSuccess,
|
|
1181
|
+
AVG(iterations) AS avgIterations
|
|
1182
|
+
FROM step_runs s ${whereSql}
|
|
1183
|
+
GROUP BY task_type, model
|
|
1184
|
+
ORDER BY task_type, avgTokensPerSuccess ASC`
|
|
1185
|
+
).all(...params);
|
|
1186
|
+
return rows.map((r) => ({
|
|
1187
|
+
taskType: String(r.taskType),
|
|
1188
|
+
model: String(r.model),
|
|
1189
|
+
steps: Number(r.steps),
|
|
1190
|
+
successRate: Number(r.successRate ?? 0),
|
|
1191
|
+
avgTokensPerSuccess: Number(r.avgTokensPerSuccess ?? 0),
|
|
1192
|
+
avgCostPerSuccess: Number(r.avgCostPerSuccess ?? 0),
|
|
1193
|
+
avgIterations: Number(r.avgIterations ?? 0)
|
|
1194
|
+
}));
|
|
1195
|
+
}
|
|
1196
|
+
function objectiveEfficiency(filter = {}) {
|
|
1197
|
+
const { whereSql, params } = dateWhere(filter, "ts");
|
|
1198
|
+
const rows = getDb().prepare(
|
|
1199
|
+
`SELECT objective,
|
|
1200
|
+
COUNT(*) AS sessions,
|
|
1201
|
+
AVG(prompt_tokens + completion_tokens) AS avgTokens,
|
|
1202
|
+
AVG(cost_usd) AS avgCostUsd,
|
|
1203
|
+
AVG(auto_score) AS avgAutoScore,
|
|
1204
|
+
AVG(user_score) AS avgUserScore
|
|
1205
|
+
FROM sessions ${whereSql}
|
|
1206
|
+
GROUP BY objective ORDER BY avgTokens ASC`
|
|
1207
|
+
).all(...params);
|
|
1208
|
+
return rows.map((r) => ({
|
|
1209
|
+
objective: String(r.objective),
|
|
1210
|
+
sessions: Number(r.sessions),
|
|
1211
|
+
avgTokens: Number(r.avgTokens ?? 0),
|
|
1212
|
+
avgCostUsd: Number(r.avgCostUsd ?? 0),
|
|
1213
|
+
avgAutoScore: r.avgAutoScore == null ? null : Number(r.avgAutoScore),
|
|
1214
|
+
avgUserScore: r.avgUserScore == null ? null : Number(r.avgUserScore)
|
|
1215
|
+
}));
|
|
1216
|
+
}
|
|
1217
|
+
function commandUsage(filter = {}) {
|
|
1218
|
+
const { whereSql, params } = dateWhere(filter, "ts");
|
|
1219
|
+
const rows = getDb().prepare(
|
|
1220
|
+
`SELECT command, COUNT(*) AS runs,
|
|
1221
|
+
SUM(prompt_tokens) AS promptTokens,
|
|
1222
|
+
SUM(completion_tokens) AS completionTokens,
|
|
1223
|
+
SUM(cost_usd) AS costUsd
|
|
1224
|
+
FROM command_runs ${whereSql}
|
|
1225
|
+
GROUP BY command ORDER BY costUsd DESC`
|
|
1226
|
+
).all(...params);
|
|
1227
|
+
return rows.map((r) => ({
|
|
1228
|
+
command: String(r.command),
|
|
1229
|
+
runs: Number(r.runs),
|
|
1230
|
+
promptTokens: Number(r.promptTokens ?? 0),
|
|
1231
|
+
completionTokens: Number(r.completionTokens ?? 0),
|
|
1232
|
+
costUsd: Number(r.costUsd ?? 0)
|
|
1233
|
+
}));
|
|
1234
|
+
}
|
|
1235
|
+
function dateWhere(filter, tsCol) {
|
|
1236
|
+
const where = [];
|
|
1237
|
+
const params = [];
|
|
1238
|
+
if (filter.since) {
|
|
1239
|
+
where.push(`date(${tsCol}/1000, 'unixepoch', 'localtime') >= ?`);
|
|
1240
|
+
params.push(filter.since);
|
|
1241
|
+
}
|
|
1242
|
+
if (filter.until) {
|
|
1243
|
+
where.push(`date(${tsCol}/1000, 'unixepoch', 'localtime') <= ?`);
|
|
1244
|
+
params.push(filter.until);
|
|
1245
|
+
}
|
|
1246
|
+
return { whereSql: where.length ? `WHERE ${where.join(" AND ")}` : "", params };
|
|
1247
|
+
}
|
|
1248
|
+
function unsyncedSessions() {
|
|
1249
|
+
const rows = getDb().prepare(`SELECT * FROM sessions WHERE synced=0 LIMIT 200`).all();
|
|
1250
|
+
return rows.map((r) => ({
|
|
1251
|
+
_table: "sessions",
|
|
1252
|
+
id: String(r.id),
|
|
1253
|
+
ts: Number(r.ts),
|
|
1254
|
+
date: String(r.date),
|
|
1255
|
+
goal: String(r.goal),
|
|
1256
|
+
command: String(r.command),
|
|
1257
|
+
objective: String(r.objective),
|
|
1258
|
+
plannedSteps: Number(r.planned_steps),
|
|
1259
|
+
completedSteps: Number(r.completed_steps),
|
|
1260
|
+
failedSteps: Number(r.failed_steps),
|
|
1261
|
+
autoScore: r.auto_score == null ? null : Number(r.auto_score),
|
|
1262
|
+
userScore: r.user_score == null ? null : Number(r.user_score),
|
|
1263
|
+
promptTokens: Number(r.prompt_tokens),
|
|
1264
|
+
completionTokens: Number(r.completion_tokens),
|
|
1265
|
+
costUsd: Number(r.cost_usd),
|
|
1266
|
+
durationMs: Number(r.duration_ms)
|
|
1267
|
+
}));
|
|
1268
|
+
}
|
|
1269
|
+
function unsyncedStepRuns() {
|
|
1270
|
+
const rows = getDb().prepare(`SELECT * FROM step_runs WHERE synced=0 LIMIT 500`).all();
|
|
1271
|
+
return rows.map((r) => ({
|
|
1272
|
+
id: Number(r.id),
|
|
1273
|
+
sessionId: String(r.session_id),
|
|
1274
|
+
stepNo: Number(r.step_no),
|
|
1275
|
+
taskType: String(r.task_type),
|
|
1276
|
+
skill: String(r.skill),
|
|
1277
|
+
model: String(r.model),
|
|
1278
|
+
provider: String(r.provider),
|
|
1279
|
+
iterations: Number(r.iterations),
|
|
1280
|
+
toolCalls: Number(r.tool_calls),
|
|
1281
|
+
promptTokens: Number(r.prompt_tokens),
|
|
1282
|
+
completionTokens: Number(r.completion_tokens),
|
|
1283
|
+
costUsd: Number(r.cost_usd),
|
|
1284
|
+
finishedBy: String(r.finished_by),
|
|
1285
|
+
success: Number(r.success) === 1,
|
|
1286
|
+
durationMs: Number(r.duration_ms)
|
|
1287
|
+
}));
|
|
1288
|
+
}
|
|
1289
|
+
function unsyncedCommandRuns() {
|
|
1290
|
+
const rows = getDb().prepare(`SELECT * FROM command_runs WHERE synced=0 LIMIT 500`).all();
|
|
1291
|
+
return rows.map((r) => ({
|
|
1292
|
+
id: Number(r.id),
|
|
1293
|
+
sessionId: r.session_id ? String(r.session_id) : void 0,
|
|
1294
|
+
ts: Number(r.ts),
|
|
1295
|
+
date: String(r.date),
|
|
1296
|
+
command: String(r.command),
|
|
1297
|
+
args: r.args ? String(r.args) : void 0,
|
|
1298
|
+
objective: r.objective ? String(r.objective) : void 0,
|
|
1299
|
+
promptTokens: Number(r.prompt_tokens),
|
|
1300
|
+
completionTokens: Number(r.completion_tokens),
|
|
1301
|
+
costUsd: Number(r.cost_usd),
|
|
1302
|
+
durationMs: Number(r.duration_ms)
|
|
1303
|
+
}));
|
|
1304
|
+
}
|
|
1305
|
+
function markTableSynced(table2, ids) {
|
|
1306
|
+
if (!ids.length) return;
|
|
1307
|
+
const stmt = getDb().prepare(`UPDATE ${table2} SET synced=1 WHERE ${table2 === "sessions" ? "id" : "id"}=?`);
|
|
1308
|
+
for (const id of ids) stmt.run(id);
|
|
1309
|
+
}
|
|
1027
1310
|
|
|
1028
1311
|
// src/usage/report.ts
|
|
1029
1312
|
function renderUsageReport(filter = {}) {
|
|
@@ -1066,6 +1349,101 @@ function renderUsageReport(filter = {}) {
|
|
|
1066
1349
|
].join("\n");
|
|
1067
1350
|
}
|
|
1068
1351
|
|
|
1352
|
+
// src/usage/analyze.ts
|
|
1353
|
+
var MIN_SUCCESS_RATE = 0.5;
|
|
1354
|
+
function renderAnalysis(filter = {}) {
|
|
1355
|
+
const out = [];
|
|
1356
|
+
const byModelTask = modelTaskEfficiency(filter);
|
|
1357
|
+
const byObjective = objectiveEfficiency(filter);
|
|
1358
|
+
const byCommand = commandUsage(filter);
|
|
1359
|
+
if (!byModelTask.length && !byObjective.length && !byCommand.length) {
|
|
1360
|
+
return c.dim('No analytics yet. Run `poly run "<task>"` a few times (and rate the result) first.');
|
|
1361
|
+
}
|
|
1362
|
+
if (byModelTask.length) {
|
|
1363
|
+
const byTask = /* @__PURE__ */ new Map();
|
|
1364
|
+
for (const r of byModelTask) {
|
|
1365
|
+
const list = byTask.get(r.taskType) ?? [];
|
|
1366
|
+
list.push(r);
|
|
1367
|
+
byTask.set(r.taskType, list);
|
|
1368
|
+
}
|
|
1369
|
+
const rows = [];
|
|
1370
|
+
for (const [task, list] of byTask) {
|
|
1371
|
+
const eligible = list.filter((r) => r.successRate >= MIN_SUCCESS_RATE && r.avgTokensPerSuccess > 0).sort((a, b) => a.avgTokensPerSuccess - b.avgTokensPerSuccess);
|
|
1372
|
+
const best = eligible[0];
|
|
1373
|
+
const runnerUp = eligible[1];
|
|
1374
|
+
if (!best) {
|
|
1375
|
+
rows.push([task, c.dim("(no reliable model yet)"), "-", "-", "-"]);
|
|
1376
|
+
continue;
|
|
1377
|
+
}
|
|
1378
|
+
rows.push([
|
|
1379
|
+
task,
|
|
1380
|
+
c.green(best.model),
|
|
1381
|
+
tokens(Math.round(best.avgTokensPerSuccess)),
|
|
1382
|
+
`${Math.round(best.successRate * 100)}%`,
|
|
1383
|
+
runnerUp ? `${runnerUp.model} ${c.dim(tokens(Math.round(runnerUp.avgTokensPerSuccess)))}` : c.dim("\u2014")
|
|
1384
|
+
]);
|
|
1385
|
+
}
|
|
1386
|
+
out.push(c.bold("Minimum-token model per task") + c.dim(` (successful steps only, success \u2265 ${MIN_SUCCESS_RATE * 100}%)`));
|
|
1387
|
+
out.push(table(["Task", "Best model", "Avg tok/success", "Success", "Runner-up"], rows));
|
|
1388
|
+
out.push("");
|
|
1389
|
+
out.push(c.bold("Model \xD7 task efficiency (all observations)"));
|
|
1390
|
+
out.push(
|
|
1391
|
+
table(
|
|
1392
|
+
["Task", "Model", "Steps", "Success", "Avg tok", "Avg iters", "Avg cost"],
|
|
1393
|
+
byModelTask.map((r) => [
|
|
1394
|
+
r.taskType,
|
|
1395
|
+
r.model,
|
|
1396
|
+
String(r.steps),
|
|
1397
|
+
`${Math.round(r.successRate * 100)}%`,
|
|
1398
|
+
r.avgTokensPerSuccess ? tokens(Math.round(r.avgTokensPerSuccess)) : c.dim("-"),
|
|
1399
|
+
r.avgIterations.toFixed(1),
|
|
1400
|
+
r.avgCostPerSuccess ? usd(r.avgCostPerSuccess) : c.dim("-")
|
|
1401
|
+
])
|
|
1402
|
+
)
|
|
1403
|
+
);
|
|
1404
|
+
out.push("");
|
|
1405
|
+
}
|
|
1406
|
+
if (byObjective.length) {
|
|
1407
|
+
out.push(c.bold("Approach efficiency") + c.dim(" (routing objective: tokens spent vs goal achievement)"));
|
|
1408
|
+
out.push(
|
|
1409
|
+
table(
|
|
1410
|
+
["Objective", "Sessions", "Avg tokens", "Avg cost", "Auto score", "Your rating"],
|
|
1411
|
+
byObjective.map((r) => [
|
|
1412
|
+
r.objective,
|
|
1413
|
+
String(r.sessions),
|
|
1414
|
+
tokens(Math.round(r.avgTokens)),
|
|
1415
|
+
usd(r.avgCostUsd),
|
|
1416
|
+
r.avgAutoScore == null ? c.dim("-") : `${Math.round(r.avgAutoScore * 100)}%`,
|
|
1417
|
+
r.avgUserScore == null ? c.dim("unrated") : `${r.avgUserScore.toFixed(1)}/9`
|
|
1418
|
+
])
|
|
1419
|
+
)
|
|
1420
|
+
);
|
|
1421
|
+
const scored = byObjective.filter((r) => r.avgAutoScore != null);
|
|
1422
|
+
if (scored.length >= 2) {
|
|
1423
|
+
const bestScore = Math.max(...scored.map((r) => r.avgAutoScore));
|
|
1424
|
+
const winner = scored.filter((r) => r.avgAutoScore >= bestScore - 0.1).sort((a, b) => a.avgTokens - b.avgTokens)[0];
|
|
1425
|
+
if (winner) {
|
|
1426
|
+
out.push(
|
|
1427
|
+
c.green(
|
|
1428
|
+
`\u2192 Lowest-token approach with top-tier achievement: "${winner.objective}" (${tokens(Math.round(winner.avgTokens))} avg tokens, ${Math.round(winner.avgAutoScore * 100)}% auto score)`
|
|
1429
|
+
)
|
|
1430
|
+
);
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
out.push("");
|
|
1434
|
+
}
|
|
1435
|
+
if (byCommand.length) {
|
|
1436
|
+
out.push(c.bold("Usage by command"));
|
|
1437
|
+
out.push(
|
|
1438
|
+
table(
|
|
1439
|
+
["Command", "Runs", "Prompt", "Compl.", "Cost"],
|
|
1440
|
+
byCommand.map((r) => [r.command, String(r.runs), tokens(r.promptTokens), tokens(r.completionTokens), usd(r.costUsd)])
|
|
1441
|
+
)
|
|
1442
|
+
);
|
|
1443
|
+
}
|
|
1444
|
+
return out.join("\n");
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1069
1447
|
// src/usage/firestoreSync.ts
|
|
1070
1448
|
async function syncUsage(config) {
|
|
1071
1449
|
if (!config.firestore.enabled) {
|
|
@@ -1119,6 +1497,225 @@ async function syncUsage(config) {
|
|
|
1119
1497
|
return { synced: rows.length, message: `Synced ${rows.length} rows to ${config.firestore.collection}.` };
|
|
1120
1498
|
}
|
|
1121
1499
|
|
|
1500
|
+
// src/usage/dataconnect.ts
|
|
1501
|
+
async function adminAccessToken(projectId) {
|
|
1502
|
+
let appMod;
|
|
1503
|
+
try {
|
|
1504
|
+
appMod = await import("firebase-admin/app");
|
|
1505
|
+
} catch {
|
|
1506
|
+
throw new Error("firebase-admin is not installed. Run `npm install firebase-admin`.");
|
|
1507
|
+
}
|
|
1508
|
+
const { initializeApp, getApps, cert, applicationDefault } = appMod;
|
|
1509
|
+
let app = getApps()[0];
|
|
1510
|
+
if (!app) {
|
|
1511
|
+
const saJson = process.env.FIREBASE_SERVICE_ACCOUNT_KEY;
|
|
1512
|
+
if (saJson) {
|
|
1513
|
+
try {
|
|
1514
|
+
app = initializeApp({ credential: cert(JSON.parse(saJson)), projectId });
|
|
1515
|
+
} catch {
|
|
1516
|
+
app = initializeApp({ credential: applicationDefault(), projectId });
|
|
1517
|
+
}
|
|
1518
|
+
} else {
|
|
1519
|
+
app = initializeApp({ credential: applicationDefault(), projectId });
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
const token = await app.options.credential.getAccessToken();
|
|
1523
|
+
return token.access_token;
|
|
1524
|
+
}
|
|
1525
|
+
async function executeGraphql(cfg2, token, query, variables) {
|
|
1526
|
+
const url = `https://firebasedataconnect.googleapis.com/v1/projects/${cfg2.projectId}/locations/${cfg2.location}/services/${cfg2.serviceId}:executeGraphql`;
|
|
1527
|
+
const res = await fetch(url, {
|
|
1528
|
+
method: "POST",
|
|
1529
|
+
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
|
|
1530
|
+
body: JSON.stringify({ query, variables })
|
|
1531
|
+
});
|
|
1532
|
+
if (!res.ok) {
|
|
1533
|
+
const text = await res.text().catch(() => "");
|
|
1534
|
+
throw new Error(`Data Connect ${res.status}: ${text.slice(0, 300)}`);
|
|
1535
|
+
}
|
|
1536
|
+
const json = await res.json();
|
|
1537
|
+
if (json.errors?.length) {
|
|
1538
|
+
throw new Error(`Data Connect GraphQL errors: ${JSON.stringify(json.errors).slice(0, 300)}`);
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
var iso = (ms) => new Date(ms).toISOString();
|
|
1542
|
+
async function syncDataConnect(config) {
|
|
1543
|
+
const dc = config.dataconnect;
|
|
1544
|
+
if (!dc?.enabled) {
|
|
1545
|
+
return { sessions: 0, steps: 0, commands: 0, calls: 0, message: "Data Connect sync is disabled (enable with `poly config dataconnect on`)." };
|
|
1546
|
+
}
|
|
1547
|
+
const projectId = config.firestore.projectId;
|
|
1548
|
+
const token = await adminAccessToken(projectId);
|
|
1549
|
+
const cfg2 = { projectId, location: dc.location, serviceId: dc.serviceId };
|
|
1550
|
+
const sessions = unsyncedSessions();
|
|
1551
|
+
for (const s of sessions) {
|
|
1552
|
+
await executeGraphql(
|
|
1553
|
+
cfg2,
|
|
1554
|
+
token,
|
|
1555
|
+
`mutation UpsertSession($id: String!, $startedAt: Timestamp!, $date: Date!, $goal: String!,
|
|
1556
|
+
$command: String!, $objective: String!, $plannedSteps: Int!, $completedSteps: Int!,
|
|
1557
|
+
$failedSteps: Int!, $autoScore: Float, $userScore: Int, $promptTokens: Int!,
|
|
1558
|
+
$completionTokens: Int!, $costUsd: Float!, $durationMs: Int!) {
|
|
1559
|
+
session_upsert(data: {
|
|
1560
|
+
id: $id, startedAt: $startedAt, date: $date, goal: $goal, command: $command,
|
|
1561
|
+
objective: $objective, plannedSteps: $plannedSteps, completedSteps: $completedSteps,
|
|
1562
|
+
failedSteps: $failedSteps, autoScore: $autoScore, userScore: $userScore,
|
|
1563
|
+
promptTokens: $promptTokens, completionTokens: $completionTokens,
|
|
1564
|
+
costUsd: $costUsd, durationMs: $durationMs
|
|
1565
|
+
})
|
|
1566
|
+
}`,
|
|
1567
|
+
{
|
|
1568
|
+
id: s.id,
|
|
1569
|
+
startedAt: iso(s.ts),
|
|
1570
|
+
date: s.date,
|
|
1571
|
+
goal: s.goal,
|
|
1572
|
+
command: s.command,
|
|
1573
|
+
objective: s.objective,
|
|
1574
|
+
plannedSteps: s.plannedSteps,
|
|
1575
|
+
completedSteps: s.completedSteps,
|
|
1576
|
+
failedSteps: s.failedSteps,
|
|
1577
|
+
autoScore: s.autoScore,
|
|
1578
|
+
userScore: s.userScore,
|
|
1579
|
+
promptTokens: s.promptTokens,
|
|
1580
|
+
completionTokens: s.completionTokens,
|
|
1581
|
+
costUsd: s.costUsd,
|
|
1582
|
+
durationMs: s.durationMs
|
|
1583
|
+
}
|
|
1584
|
+
);
|
|
1585
|
+
}
|
|
1586
|
+
markTableSynced("sessions", sessions.map((s) => s.id));
|
|
1587
|
+
const steps = unsyncedStepRuns();
|
|
1588
|
+
for (const st of steps) {
|
|
1589
|
+
await executeGraphql(
|
|
1590
|
+
cfg2,
|
|
1591
|
+
token,
|
|
1592
|
+
`mutation InsertStep($sessionId: String!, $stepNo: Int!, $taskType: String!, $skill: String!,
|
|
1593
|
+
$model: String!, $provider: String!, $iterations: Int!, $toolCalls: Int!,
|
|
1594
|
+
$promptTokens: Int!, $completionTokens: Int!, $costUsd: Float!,
|
|
1595
|
+
$finishedBy: String!, $success: Boolean!, $durationMs: Int!) {
|
|
1596
|
+
stepRun_insert(data: {
|
|
1597
|
+
sessionId: $sessionId, stepNo: $stepNo, taskType: $taskType, skill: $skill,
|
|
1598
|
+
model: $model, provider: $provider, iterations: $iterations, toolCalls: $toolCalls,
|
|
1599
|
+
promptTokens: $promptTokens, completionTokens: $completionTokens, costUsd: $costUsd,
|
|
1600
|
+
finishedBy: $finishedBy, success: $success, durationMs: $durationMs
|
|
1601
|
+
})
|
|
1602
|
+
}`,
|
|
1603
|
+
{
|
|
1604
|
+
sessionId: st.sessionId,
|
|
1605
|
+
stepNo: st.stepNo,
|
|
1606
|
+
taskType: st.taskType,
|
|
1607
|
+
skill: st.skill,
|
|
1608
|
+
model: st.model,
|
|
1609
|
+
provider: st.provider,
|
|
1610
|
+
iterations: st.iterations,
|
|
1611
|
+
toolCalls: st.toolCalls,
|
|
1612
|
+
promptTokens: st.promptTokens,
|
|
1613
|
+
completionTokens: st.completionTokens,
|
|
1614
|
+
costUsd: st.costUsd,
|
|
1615
|
+
finishedBy: st.finishedBy,
|
|
1616
|
+
success: st.success,
|
|
1617
|
+
durationMs: st.durationMs
|
|
1618
|
+
}
|
|
1619
|
+
);
|
|
1620
|
+
}
|
|
1621
|
+
markTableSynced("step_runs", steps.map((s) => s.id));
|
|
1622
|
+
const commands = unsyncedCommandRuns();
|
|
1623
|
+
for (const cr of commands) {
|
|
1624
|
+
await executeGraphql(
|
|
1625
|
+
cfg2,
|
|
1626
|
+
token,
|
|
1627
|
+
`mutation InsertCommand($sessionId: String, $ts: Timestamp!, $date: Date!, $command: String!,
|
|
1628
|
+
$args: String, $objective: String, $promptTokens: Int!, $completionTokens: Int!,
|
|
1629
|
+
$costUsd: Float!, $durationMs: Int!) {
|
|
1630
|
+
commandRun_insert(data: {
|
|
1631
|
+
sessionId: $sessionId, ts: $ts, date: $date, command: $command, args: $args,
|
|
1632
|
+
objective: $objective, promptTokens: $promptTokens, completionTokens: $completionTokens,
|
|
1633
|
+
costUsd: $costUsd, durationMs: $durationMs
|
|
1634
|
+
})
|
|
1635
|
+
}`,
|
|
1636
|
+
{
|
|
1637
|
+
sessionId: cr.sessionId ?? null,
|
|
1638
|
+
ts: iso(cr.ts),
|
|
1639
|
+
date: cr.date,
|
|
1640
|
+
command: cr.command,
|
|
1641
|
+
args: cr.args ?? null,
|
|
1642
|
+
objective: cr.objective ?? null,
|
|
1643
|
+
promptTokens: cr.promptTokens,
|
|
1644
|
+
completionTokens: cr.completionTokens,
|
|
1645
|
+
costUsd: cr.costUsd,
|
|
1646
|
+
durationMs: cr.durationMs
|
|
1647
|
+
}
|
|
1648
|
+
);
|
|
1649
|
+
}
|
|
1650
|
+
markTableSynced("command_runs", commands.map((c2) => c2.id));
|
|
1651
|
+
const calls = unsyncedRows();
|
|
1652
|
+
for (const u of calls) {
|
|
1653
|
+
await executeGraphql(
|
|
1654
|
+
cfg2,
|
|
1655
|
+
token,
|
|
1656
|
+
`mutation InsertCall($sessionId: String, $ts: Timestamp!, $date: Date!, $command: String!,
|
|
1657
|
+
$taskType: String!, $model: String!, $provider: String!, $promptTokens: Int!,
|
|
1658
|
+
$completionTokens: Int!, $totalTokens: Int!, $costUsd: Float!) {
|
|
1659
|
+
modelCall_insert(data: {
|
|
1660
|
+
sessionId: $sessionId, ts: $ts, date: $date, command: $command, taskType: $taskType,
|
|
1661
|
+
model: $model, provider: $provider, promptTokens: $promptTokens,
|
|
1662
|
+
completionTokens: $completionTokens, totalTokens: $totalTokens, costUsd: $costUsd
|
|
1663
|
+
})
|
|
1664
|
+
}`,
|
|
1665
|
+
{
|
|
1666
|
+
sessionId: u.sessionId ?? null,
|
|
1667
|
+
ts: iso(u.ts),
|
|
1668
|
+
date: u.date,
|
|
1669
|
+
command: u.command ?? "run",
|
|
1670
|
+
taskType: u.taskType,
|
|
1671
|
+
model: u.model,
|
|
1672
|
+
provider: u.provider,
|
|
1673
|
+
promptTokens: u.promptTokens,
|
|
1674
|
+
completionTokens: u.completionTokens,
|
|
1675
|
+
totalTokens: u.totalTokens,
|
|
1676
|
+
costUsd: u.costUsd
|
|
1677
|
+
}
|
|
1678
|
+
);
|
|
1679
|
+
}
|
|
1680
|
+
markSynced(calls.map((c2) => c2.id));
|
|
1681
|
+
return {
|
|
1682
|
+
sessions: sessions.length,
|
|
1683
|
+
steps: steps.length,
|
|
1684
|
+
commands: commands.length,
|
|
1685
|
+
calls: calls.length,
|
|
1686
|
+
message: `Synced ${sessions.length} sessions, ${steps.length} steps, ${commands.length} commands, ${calls.length} calls to Data Connect (${cfg2.serviceId}@${cfg2.location}).`
|
|
1687
|
+
};
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
// src/usage/logger.ts
|
|
1691
|
+
function localDate(d = /* @__PURE__ */ new Date()) {
|
|
1692
|
+
const y = d.getFullYear();
|
|
1693
|
+
const m = String(d.getMonth() + 1).padStart(2, "0");
|
|
1694
|
+
const day = String(d.getDate()).padStart(2, "0");
|
|
1695
|
+
return `${y}-${m}-${day}`;
|
|
1696
|
+
}
|
|
1697
|
+
function providerOf(modelId) {
|
|
1698
|
+
return modelId.split("/")[0] ?? "unknown";
|
|
1699
|
+
}
|
|
1700
|
+
function logCompletion(result, taskType, sessionId, command = "run") {
|
|
1701
|
+
const now = /* @__PURE__ */ new Date();
|
|
1702
|
+
const entry = {
|
|
1703
|
+
ts: now.getTime(),
|
|
1704
|
+
date: localDate(now),
|
|
1705
|
+
provider: providerOf(result.model),
|
|
1706
|
+
model: result.model,
|
|
1707
|
+
taskType,
|
|
1708
|
+
promptTokens: result.usage.promptTokens,
|
|
1709
|
+
completionTokens: result.usage.completionTokens,
|
|
1710
|
+
totalTokens: result.usage.totalTokens,
|
|
1711
|
+
costUsd: result.costUsd,
|
|
1712
|
+
sessionId,
|
|
1713
|
+
command
|
|
1714
|
+
};
|
|
1715
|
+
recordUsage(entry);
|
|
1716
|
+
return entry;
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1122
1719
|
// src/tui/App.tsx
|
|
1123
1720
|
import { useState, useEffect, useCallback } from "react";
|
|
1124
1721
|
import { Box, Text, useApp, useInput } from "ink";
|
|
@@ -1274,46 +1871,31 @@ ${stderr}`)) };
|
|
|
1274
1871
|
}
|
|
1275
1872
|
}
|
|
1276
1873
|
|
|
1277
|
-
// src/usage/logger.ts
|
|
1278
|
-
function localDate(d = /* @__PURE__ */ new Date()) {
|
|
1279
|
-
const y = d.getFullYear();
|
|
1280
|
-
const m = String(d.getMonth() + 1).padStart(2, "0");
|
|
1281
|
-
const day = String(d.getDate()).padStart(2, "0");
|
|
1282
|
-
return `${y}-${m}-${day}`;
|
|
1283
|
-
}
|
|
1284
|
-
function providerOf(modelId) {
|
|
1285
|
-
return modelId.split("/")[0] ?? "unknown";
|
|
1286
|
-
}
|
|
1287
|
-
function logCompletion(result, taskType, sessionId) {
|
|
1288
|
-
const now = /* @__PURE__ */ new Date();
|
|
1289
|
-
const entry = {
|
|
1290
|
-
ts: now.getTime(),
|
|
1291
|
-
date: localDate(now),
|
|
1292
|
-
provider: providerOf(result.model),
|
|
1293
|
-
model: result.model,
|
|
1294
|
-
taskType,
|
|
1295
|
-
promptTokens: result.usage.promptTokens,
|
|
1296
|
-
completionTokens: result.usage.completionTokens,
|
|
1297
|
-
totalTokens: result.usage.totalTokens,
|
|
1298
|
-
costUsd: result.costUsd,
|
|
1299
|
-
sessionId
|
|
1300
|
-
};
|
|
1301
|
-
recordUsage(entry);
|
|
1302
|
-
return entry;
|
|
1303
|
-
}
|
|
1304
|
-
|
|
1305
1874
|
// src/agent/loop.ts
|
|
1306
1875
|
var MAX_ITERS_PER_STEP = 6;
|
|
1307
1876
|
async function runAgent(goal, deps, emit) {
|
|
1308
1877
|
const { client: client2, models, policy, sessionId, cwd } = deps;
|
|
1309
1878
|
let totalCostUsd = 0;
|
|
1310
1879
|
let totalTokens = 0;
|
|
1880
|
+
let totalPromptTokens = 0;
|
|
1881
|
+
let totalCompletionTokens = 0;
|
|
1311
1882
|
let calls = 0;
|
|
1883
|
+
const sessionStart = Date.now();
|
|
1884
|
+
let completedSteps = 0;
|
|
1885
|
+
let failedSteps = 0;
|
|
1312
1886
|
const planRoute = route("plan", models, policy);
|
|
1313
1887
|
let plan;
|
|
1314
1888
|
if (planRoute) {
|
|
1315
1889
|
try {
|
|
1316
|
-
plan = await planRequest(goal, client2, planRoute.model)
|
|
1890
|
+
plan = await planRequest(goal, client2, planRoute.model, (result) => {
|
|
1891
|
+
const entry = logCompletion(result, "plan", sessionId);
|
|
1892
|
+
emit({ type: "usage", entry });
|
|
1893
|
+
totalCostUsd += entry.costUsd;
|
|
1894
|
+
totalTokens += entry.totalTokens;
|
|
1895
|
+
totalPromptTokens += entry.promptTokens;
|
|
1896
|
+
totalCompletionTokens += entry.completionTokens;
|
|
1897
|
+
calls++;
|
|
1898
|
+
});
|
|
1317
1899
|
} catch {
|
|
1318
1900
|
plan = heuristicPlan(goal);
|
|
1319
1901
|
}
|
|
@@ -1321,6 +1903,15 @@ async function runAgent(goal, deps, emit) {
|
|
|
1321
1903
|
plan = heuristicPlan(goal);
|
|
1322
1904
|
}
|
|
1323
1905
|
emit({ type: "plan", plan, planModel: planRoute?.model.id ?? "heuristic" });
|
|
1906
|
+
startSession({
|
|
1907
|
+
id: sessionId,
|
|
1908
|
+
ts: sessionStart,
|
|
1909
|
+
date: localDate2(),
|
|
1910
|
+
goal,
|
|
1911
|
+
command: "run",
|
|
1912
|
+
objective: policy.objective,
|
|
1913
|
+
plannedSteps: plan.steps.length
|
|
1914
|
+
});
|
|
1324
1915
|
const toolCtx = {
|
|
1325
1916
|
cwd,
|
|
1326
1917
|
allowWrite: deps.allowWrite,
|
|
@@ -1333,6 +1924,7 @@ async function runAgent(goal, deps, emit) {
|
|
|
1333
1924
|
completionTokens: step.estCompletionTokens
|
|
1334
1925
|
});
|
|
1335
1926
|
if (!r) {
|
|
1927
|
+
failedSteps++;
|
|
1336
1928
|
emit({ type: "error", message: `No capable model for step ${step.id} (${step.type}).` });
|
|
1337
1929
|
continue;
|
|
1338
1930
|
}
|
|
@@ -1343,55 +1935,113 @@ async function runAgent(goal, deps, emit) {
|
|
|
1343
1935
|
{ role: "system", content: stepSystemPrompt(goal, step, priorSummaries, useTools) },
|
|
1344
1936
|
{ role: "user", content: step.description }
|
|
1345
1937
|
];
|
|
1938
|
+
const stepStart = Date.now();
|
|
1939
|
+
let stepPrompt = 0;
|
|
1940
|
+
let stepCompletion = 0;
|
|
1941
|
+
let stepCost = 0;
|
|
1942
|
+
let stepToolCalls = 0;
|
|
1943
|
+
let iterations = 0;
|
|
1944
|
+
let finishedBy = "max-iters";
|
|
1346
1945
|
let summary = "";
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
next
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1946
|
+
try {
|
|
1947
|
+
for (let iter = 0; iter < MAX_ITERS_PER_STEP; iter++) {
|
|
1948
|
+
iterations = iter + 1;
|
|
1949
|
+
const gen = client2.stream(
|
|
1950
|
+
{
|
|
1951
|
+
model: model.id,
|
|
1952
|
+
messages,
|
|
1953
|
+
tools: useTools ? TOOL_SCHEMAS : void 0,
|
|
1954
|
+
temperature: 0.2,
|
|
1955
|
+
maxTokens: 2e3
|
|
1956
|
+
},
|
|
1957
|
+
model.pricing
|
|
1958
|
+
);
|
|
1959
|
+
let next = await gen.next();
|
|
1960
|
+
while (!next.done) {
|
|
1961
|
+
emit({ type: "text", delta: next.value });
|
|
1962
|
+
next = await gen.next();
|
|
1963
|
+
}
|
|
1964
|
+
const result = next.value;
|
|
1965
|
+
const entry = logCompletion(result, step.type, sessionId);
|
|
1966
|
+
emit({ type: "usage", entry });
|
|
1967
|
+
totalCostUsd += entry.costUsd;
|
|
1968
|
+
totalTokens += entry.totalTokens;
|
|
1969
|
+
totalPromptTokens += entry.promptTokens;
|
|
1970
|
+
totalCompletionTokens += entry.completionTokens;
|
|
1971
|
+
stepPrompt += entry.promptTokens;
|
|
1972
|
+
stepCompletion += entry.completionTokens;
|
|
1973
|
+
stepCost += entry.costUsd;
|
|
1974
|
+
calls++;
|
|
1975
|
+
if (result.toolCalls.length && useTools) {
|
|
1976
|
+
messages.push({ role: "assistant", content: result.content, tool_calls: result.toolCalls });
|
|
1977
|
+
let finished = false;
|
|
1978
|
+
for (const tc of result.toolCalls) {
|
|
1979
|
+
stepToolCalls++;
|
|
1980
|
+
emit({ type: "tool-call", name: tc.function.name, args: tc.function.arguments });
|
|
1981
|
+
const outcome = executeTool(tc.function.name, tc.function.arguments, toolCtx);
|
|
1982
|
+
emit({ type: "tool-result", name: tc.function.name, result: outcome.result });
|
|
1983
|
+
messages.push({ role: "tool", tool_call_id: tc.id, name: tc.function.name, content: outcome.result });
|
|
1984
|
+
if (outcome.finishSummary != null) {
|
|
1985
|
+
summary = outcome.finishSummary;
|
|
1986
|
+
finished = true;
|
|
1987
|
+
}
|
|
1380
1988
|
}
|
|
1989
|
+
if (finished) {
|
|
1990
|
+
finishedBy = "finish-tool";
|
|
1991
|
+
break;
|
|
1992
|
+
}
|
|
1993
|
+
continue;
|
|
1381
1994
|
}
|
|
1382
|
-
|
|
1383
|
-
|
|
1995
|
+
summary = result.content || summary;
|
|
1996
|
+
if (summary) finishedBy = "text";
|
|
1997
|
+
break;
|
|
1384
1998
|
}
|
|
1385
|
-
|
|
1386
|
-
|
|
1999
|
+
} catch (err) {
|
|
2000
|
+
finishedBy = "error";
|
|
2001
|
+
emit({ type: "error", message: `Step ${step.id} failed: ${err?.message ?? err}` });
|
|
1387
2002
|
}
|
|
2003
|
+
const success = finishedBy === "finish-tool" || finishedBy === "text";
|
|
2004
|
+
if (success) completedSteps++;
|
|
2005
|
+
else failedSteps++;
|
|
2006
|
+
recordStepRun({
|
|
2007
|
+
sessionId,
|
|
2008
|
+
stepNo: step.id,
|
|
2009
|
+
taskType: step.type,
|
|
2010
|
+
skill: TASK_SKILL[step.type],
|
|
2011
|
+
model: model.id,
|
|
2012
|
+
provider: model.provider,
|
|
2013
|
+
iterations,
|
|
2014
|
+
toolCalls: stepToolCalls,
|
|
2015
|
+
promptTokens: stepPrompt,
|
|
2016
|
+
completionTokens: stepCompletion,
|
|
2017
|
+
costUsd: stepCost,
|
|
2018
|
+
finishedBy,
|
|
2019
|
+
success,
|
|
2020
|
+
durationMs: Date.now() - stepStart
|
|
2021
|
+
});
|
|
1388
2022
|
if (!summary) summary = "(no summary)";
|
|
1389
2023
|
priorSummaries.push(`Step ${step.id} (${step.type}): ${summary}`);
|
|
1390
2024
|
emit({ type: "step-end", step, summary });
|
|
1391
2025
|
}
|
|
2026
|
+
finishSession(sessionId, {
|
|
2027
|
+
plannedSteps: plan.steps.length,
|
|
2028
|
+
completedSteps,
|
|
2029
|
+
failedSteps,
|
|
2030
|
+
autoScore: plan.steps.length ? completedSteps / plan.steps.length : null,
|
|
2031
|
+
promptTokens: totalPromptTokens,
|
|
2032
|
+
completionTokens: totalCompletionTokens,
|
|
2033
|
+
costUsd: totalCostUsd,
|
|
2034
|
+
durationMs: Date.now() - sessionStart
|
|
2035
|
+
});
|
|
1392
2036
|
emit({ type: "done", totalCostUsd, totalTokens, calls });
|
|
1393
2037
|
return { totalCostUsd, totalTokens, calls };
|
|
1394
2038
|
}
|
|
2039
|
+
function localDate2(d = /* @__PURE__ */ new Date()) {
|
|
2040
|
+
const y = d.getFullYear();
|
|
2041
|
+
const m = String(d.getMonth() + 1).padStart(2, "0");
|
|
2042
|
+
const day = String(d.getDate()).padStart(2, "0");
|
|
2043
|
+
return `${y}-${m}-${day}`;
|
|
2044
|
+
}
|
|
1395
2045
|
function stepSystemPrompt(goal, step, priorSummaries, useTools) {
|
|
1396
2046
|
const context = priorSummaries.length ? `
|
|
1397
2047
|
|
|
@@ -1418,6 +2068,7 @@ function App(props) {
|
|
|
1418
2068
|
const [cost, setCost] = useState(0);
|
|
1419
2069
|
const [tok, setTok] = useState(0);
|
|
1420
2070
|
const [calls, setCalls] = useState(0);
|
|
2071
|
+
const [rated, setRated] = useState(null);
|
|
1421
2072
|
const push = useCallback((text, color) => {
|
|
1422
2073
|
setLog((l) => [...l, { key: l.length, text, color }]);
|
|
1423
2074
|
}, []);
|
|
@@ -1484,7 +2135,7 @@ function App(props) {
|
|
|
1484
2135
|
} catch (err) {
|
|
1485
2136
|
push(`Fatal: ${err?.message ?? err}`, "red");
|
|
1486
2137
|
}
|
|
1487
|
-
setPhase("
|
|
2138
|
+
setPhase("rate");
|
|
1488
2139
|
}, [goal, props, push]);
|
|
1489
2140
|
useInput((input, key) => {
|
|
1490
2141
|
if (phase === "preview") {
|
|
@@ -1493,6 +2144,18 @@ function App(props) {
|
|
|
1493
2144
|
setDraft(goal);
|
|
1494
2145
|
setPhase("input");
|
|
1495
2146
|
} else if (input === "q") exit();
|
|
2147
|
+
} else if (phase === "rate") {
|
|
2148
|
+
if (/^[0-9]$/.test(input)) {
|
|
2149
|
+
const score = parseInt(input, 10);
|
|
2150
|
+
try {
|
|
2151
|
+
setUserScore(props.sessionId, score);
|
|
2152
|
+
} catch {
|
|
2153
|
+
}
|
|
2154
|
+
setRated(score);
|
|
2155
|
+
setPhase("done");
|
|
2156
|
+
} else if (key.return || input === "q") {
|
|
2157
|
+
setPhase("done");
|
|
2158
|
+
}
|
|
1496
2159
|
} else if (phase === "done") {
|
|
1497
2160
|
if (input === "q" || key.return) exit();
|
|
1498
2161
|
}
|
|
@@ -1516,12 +2179,26 @@ function App(props) {
|
|
|
1516
2179
|
)
|
|
1517
2180
|
] }),
|
|
1518
2181
|
phase === "preview" && rec && /* @__PURE__ */ jsx(Preview, { rec }),
|
|
1519
|
-
(phase === "running" || phase === "done") && /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: 1, children: [
|
|
2182
|
+
(phase === "running" || phase === "rate" || phase === "done") && /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: 1, children: [
|
|
1520
2183
|
log.slice(-18).map((l) => /* @__PURE__ */ jsx(Text, { color: l.color, children: l.text }, l.key)),
|
|
1521
2184
|
phase === "running" && /* @__PURE__ */ jsxs(Text, { color: "cyan", children: [
|
|
1522
2185
|
/* @__PURE__ */ jsx(Spinner, { type: "dots" }),
|
|
1523
2186
|
" working\u2026"
|
|
1524
2187
|
] }),
|
|
2188
|
+
phase === "rate" && /* @__PURE__ */ jsxs(Text, { children: [
|
|
2189
|
+
/* @__PURE__ */ jsxs(Text, { color: "green", children: [
|
|
2190
|
+
"\u2713 Done \xB7 ",
|
|
2191
|
+
calls,
|
|
2192
|
+
" calls \xB7 ",
|
|
2193
|
+
tokens(tok),
|
|
2194
|
+
" tokens \xB7 ",
|
|
2195
|
+
usd(cost)
|
|
2196
|
+
] }),
|
|
2197
|
+
"\n",
|
|
2198
|
+
/* @__PURE__ */ jsx(Text, { color: "cyan", children: "How well was your goal achieved? " }),
|
|
2199
|
+
/* @__PURE__ */ jsx(Text, { color: "yellow", children: "[0-9]" }),
|
|
2200
|
+
/* @__PURE__ */ jsx(Text, { color: "gray", children: " (9 = perfect \xB7 enter = skip) \u2014 feeds `poly analyze`" })
|
|
2201
|
+
] }),
|
|
1525
2202
|
phase === "done" && /* @__PURE__ */ jsxs(Text, { color: "green", children: [
|
|
1526
2203
|
"\u2713 Done \xB7 ",
|
|
1527
2204
|
calls,
|
|
@@ -1529,6 +2206,7 @@ function App(props) {
|
|
|
1529
2206
|
tokens(tok),
|
|
1530
2207
|
" tokens \xB7 ",
|
|
1531
2208
|
usd(cost),
|
|
2209
|
+
rated != null ? ` \xB7 rated ${rated}/9` : "",
|
|
1532
2210
|
" \u2014 press q to quit"
|
|
1533
2211
|
] })
|
|
1534
2212
|
] })
|
|
@@ -1596,7 +2274,7 @@ function truncate2(s, n) {
|
|
|
1596
2274
|
|
|
1597
2275
|
// src/index.ts
|
|
1598
2276
|
var program = new Command();
|
|
1599
|
-
program.name("poly").description("Polymath \u2014 cost-optimized, multi-model TUI coding agent").version("0.
|
|
2277
|
+
program.name("poly").description("Polymath \u2014 cost-optimized, multi-model TUI coding agent").version("0.2.0");
|
|
1600
2278
|
function client(config) {
|
|
1601
2279
|
return new OpenRouterClient({
|
|
1602
2280
|
apiKey: resolveApiKey(config),
|
|
@@ -1613,6 +2291,29 @@ function buildPolicy(config, opts) {
|
|
|
1613
2291
|
pinned: config.pinned
|
|
1614
2292
|
};
|
|
1615
2293
|
}
|
|
2294
|
+
function localDate3(d = /* @__PURE__ */ new Date()) {
|
|
2295
|
+
const y = d.getFullYear();
|
|
2296
|
+
const m = String(d.getMonth() + 1).padStart(2, "0");
|
|
2297
|
+
const day = String(d.getDate()).padStart(2, "0");
|
|
2298
|
+
return `${y}-${m}-${day}`;
|
|
2299
|
+
}
|
|
2300
|
+
function trackCommand(opts) {
|
|
2301
|
+
try {
|
|
2302
|
+
recordCommandRun({
|
|
2303
|
+
sessionId: opts.sessionId,
|
|
2304
|
+
ts: opts.startedAt,
|
|
2305
|
+
date: localDate3(new Date(opts.startedAt)),
|
|
2306
|
+
command: opts.command,
|
|
2307
|
+
args: opts.args?.slice(0, 300),
|
|
2308
|
+
objective: opts.objective,
|
|
2309
|
+
promptTokens: opts.promptTokens ?? 0,
|
|
2310
|
+
completionTokens: opts.completionTokens ?? 0,
|
|
2311
|
+
costUsd: opts.costUsd ?? 0,
|
|
2312
|
+
durationMs: Date.now() - opts.startedAt
|
|
2313
|
+
});
|
|
2314
|
+
} catch {
|
|
2315
|
+
}
|
|
2316
|
+
}
|
|
1616
2317
|
async function loadCatalog(config, refresh = false) {
|
|
1617
2318
|
const models = await getModels(client(config), { refresh });
|
|
1618
2319
|
if (!models.length) {
|
|
@@ -1625,6 +2326,7 @@ program.command("login").description("Connect Polymath to OpenRouter (set/replac
|
|
|
1625
2326
|
await runLogin();
|
|
1626
2327
|
});
|
|
1627
2328
|
program.command("run", { isDefault: true }).description("Launch the interactive agent (TUI)").argument("[goal...]", "what to do (optional; prompts if omitted)").option("-o, --objective <name>", "routing objective: cheapest | value | quality").option("--max-cost <usd>", "exclude models whose projected per-call cost exceeds this").option("-w, --write", "allow the agent to write files (confined to --cwd)", false).option("-x, --commands", "DANGER: let the model run arbitrary shell commands in --cwd", false).option("-C, --cwd <dir>", "working directory", process.cwd()).action(async (goalParts, opts) => {
|
|
2329
|
+
const startedAt = Date.now();
|
|
1628
2330
|
const config = loadConfig();
|
|
1629
2331
|
const key = await ensureApiKey(config);
|
|
1630
2332
|
if (!key) {
|
|
@@ -1635,12 +2337,13 @@ program.command("run", { isDefault: true }).description("Launch the interactive
|
|
|
1635
2337
|
const models = await loadCatalog(reloaded);
|
|
1636
2338
|
const policy = buildPolicy(reloaded, opts);
|
|
1637
2339
|
const goal = goalParts?.join(" ").trim() || void 0;
|
|
2340
|
+
const sessionId = randomUUID();
|
|
1638
2341
|
const instance = render(
|
|
1639
2342
|
createElement(App, {
|
|
1640
2343
|
client: client(reloaded),
|
|
1641
2344
|
models,
|
|
1642
2345
|
policy,
|
|
1643
|
-
sessionId
|
|
2346
|
+
sessionId,
|
|
1644
2347
|
cwd: opts.cwd,
|
|
1645
2348
|
allowWrite: !!opts.write,
|
|
1646
2349
|
allowCommands: !!opts.commands,
|
|
@@ -1649,11 +2352,22 @@ program.command("run", { isDefault: true }).description("Launch the interactive
|
|
|
1649
2352
|
})
|
|
1650
2353
|
);
|
|
1651
2354
|
await instance.waitUntilExit();
|
|
2355
|
+
const totals2 = sessionUsageTotals(sessionId);
|
|
2356
|
+
trackCommand({
|
|
2357
|
+
command: "run",
|
|
2358
|
+
startedAt,
|
|
2359
|
+
sessionId,
|
|
2360
|
+
args: goal,
|
|
2361
|
+
objective: policy.objective,
|
|
2362
|
+
...totals2
|
|
2363
|
+
});
|
|
1652
2364
|
});
|
|
1653
2365
|
program.command("recommend").description("Recommend the best / best-value model combos for a task BEFORE running").argument("<goal...>", "task description").option("--smart", "use an LLM to produce a tailored plan (costs a few cents)", false).option("-o, --objective <name>", "highlight a specific objective").action(async (goalParts, opts) => {
|
|
2366
|
+
const startedAt = Date.now();
|
|
1654
2367
|
const config = loadConfig();
|
|
1655
2368
|
const models = await loadCatalog(config);
|
|
1656
2369
|
const goal = goalParts.join(" ");
|
|
2370
|
+
const sessionId = randomUUID();
|
|
1657
2371
|
let plan = heuristicPlan(goal);
|
|
1658
2372
|
if (opts.smart) {
|
|
1659
2373
|
const key = resolveApiKey(config);
|
|
@@ -1663,7 +2377,9 @@ program.command("recommend").description("Recommend the best / best-value model
|
|
|
1663
2377
|
const planRoute = route("plan", models, buildPolicy(config, {}));
|
|
1664
2378
|
if (planRoute) {
|
|
1665
2379
|
try {
|
|
1666
|
-
plan = await planRequest(goal, client(config), planRoute.model)
|
|
2380
|
+
plan = await planRequest(goal, client(config), planRoute.model, (result) => {
|
|
2381
|
+
logCompletion(result, "plan", sessionId, "recommend");
|
|
2382
|
+
});
|
|
1667
2383
|
} catch (e) {
|
|
1668
2384
|
console.error(c.yellow(`Smart plan failed (${e?.message}); using heuristic.`));
|
|
1669
2385
|
}
|
|
@@ -1671,6 +2387,8 @@ program.command("recommend").description("Recommend the best / best-value model
|
|
|
1671
2387
|
}
|
|
1672
2388
|
}
|
|
1673
2389
|
console.log(renderRecommendation(buildRecommendation(plan, models)));
|
|
2390
|
+
const totals2 = sessionUsageTotals(sessionId);
|
|
2391
|
+
trackCommand({ command: "recommend", startedAt, sessionId, args: goal, objective: config.defaultObjective, ...totals2 });
|
|
1674
2392
|
});
|
|
1675
2393
|
program.command("models").description("Browse the model catalog with pricing and tiers").option("-t, --tier <tier>", "filter by tier: cheap | standard | frontier").option("--tools", "only models that support tool/function calling", false).option("-s, --search <text>", "filter by id/name substring").option("--refresh", "force-refresh the catalog from OpenRouter", false).option("-n, --limit <n>", "max rows", "40").action(async (opts) => {
|
|
1676
2394
|
const config = loadConfig();
|
|
@@ -1701,11 +2419,11 @@ program.command("usage").description("Show recorded usage & cost by date + model
|
|
|
1701
2419
|
let until = opts.until;
|
|
1702
2420
|
if (opts.today) {
|
|
1703
2421
|
const d = /* @__PURE__ */ new Date();
|
|
1704
|
-
const
|
|
2422
|
+
const iso2 = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(
|
|
1705
2423
|
d.getDate()
|
|
1706
2424
|
).padStart(2, "0")}`;
|
|
1707
|
-
since =
|
|
1708
|
-
until =
|
|
2425
|
+
since = iso2;
|
|
2426
|
+
until = iso2;
|
|
1709
2427
|
}
|
|
1710
2428
|
console.log(renderUsageReport({ since, until }));
|
|
1711
2429
|
if (opts.sync) {
|
|
@@ -1713,10 +2431,33 @@ program.command("usage").description("Show recorded usage & cost by date + model
|
|
|
1713
2431
|
console.log(res.synced > 0 ? c.green(res.message) : c.dim(res.message));
|
|
1714
2432
|
}
|
|
1715
2433
|
});
|
|
1716
|
-
program.command("
|
|
2434
|
+
program.command("analyze").description("Which approach reaches the goal with the FEWEST tokens \u2014 per model, task, objective, command").option("--since <date>", "YYYY-MM-DD inclusive").option("--until <date>", "YYYY-MM-DD inclusive").action(async (opts) => {
|
|
2435
|
+
console.log(renderAnalysis({ since: opts.since, until: opts.until }));
|
|
2436
|
+
});
|
|
2437
|
+
program.command("sync").description("Push the local analytics ledger to Firebase (Data Connect SQL and/or Firestore)").action(async () => {
|
|
1717
2438
|
const config = loadConfig();
|
|
1718
|
-
|
|
1719
|
-
|
|
2439
|
+
let pushed = false;
|
|
2440
|
+
if (config.dataconnect.enabled) {
|
|
2441
|
+
pushed = true;
|
|
2442
|
+
try {
|
|
2443
|
+
const res = await syncDataConnect(config);
|
|
2444
|
+
console.log(res.sessions + res.steps + res.commands + res.calls > 0 ? c.green(res.message) : c.dim(res.message));
|
|
2445
|
+
} catch (e) {
|
|
2446
|
+
console.error(c.red(`Data Connect sync failed: ${e?.message ?? e}`));
|
|
2447
|
+
}
|
|
2448
|
+
}
|
|
2449
|
+
if (config.firestore.enabled) {
|
|
2450
|
+
pushed = true;
|
|
2451
|
+
const res = await syncUsage(config);
|
|
2452
|
+
console.log(res.synced > 0 ? c.green(res.message) : c.dim(res.message));
|
|
2453
|
+
}
|
|
2454
|
+
if (!pushed) {
|
|
2455
|
+
console.log(
|
|
2456
|
+
c.yellow(
|
|
2457
|
+
"No sync target enabled. Use `poly config dataconnect on` (SQL) or `poly config firestore on`."
|
|
2458
|
+
)
|
|
2459
|
+
);
|
|
2460
|
+
}
|
|
1720
2461
|
});
|
|
1721
2462
|
var cfg = program.command("config").description("View or change Polymath settings");
|
|
1722
2463
|
cfg.command("show").description("Print the current config (key is masked)").action(() => {
|
|
@@ -1758,6 +2499,18 @@ cfg.command("firestore").description("Enable/disable Firestore sync: on | off").
|
|
|
1758
2499
|
saveConfig(config);
|
|
1759
2500
|
console.log(c.green(`Firestore sync ${config.firestore.enabled ? "enabled" : "disabled"}.`));
|
|
1760
2501
|
});
|
|
2502
|
+
cfg.command("dataconnect").description("Enable/disable Firebase Data Connect (SQL) sync: on | off [--location <loc>] [--service <id>]").argument("<state>").option("--location <loc>", "Data Connect location (default us-east4)").option("--service <id>", "Data Connect service id (default polymath)").action((state, opts) => {
|
|
2503
|
+
const config = loadConfig();
|
|
2504
|
+
config.dataconnect.enabled = /^on|true|1$/i.test(state);
|
|
2505
|
+
if (opts.location) config.dataconnect.location = opts.location;
|
|
2506
|
+
if (opts.service) config.dataconnect.serviceId = opts.service;
|
|
2507
|
+
saveConfig(config);
|
|
2508
|
+
console.log(
|
|
2509
|
+
c.green(
|
|
2510
|
+
`Data Connect sync ${config.dataconnect.enabled ? "enabled" : "disabled"} (service ${config.dataconnect.serviceId} @ ${config.dataconnect.location}).`
|
|
2511
|
+
)
|
|
2512
|
+
);
|
|
2513
|
+
});
|
|
1761
2514
|
program.parseAsync().catch((err) => {
|
|
1762
2515
|
console.error(c.red(err?.message ?? String(err)));
|
|
1763
2516
|
process.exit(1);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "polymath-agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Polymath — a cost-optimized, multi-model TUI coding agent. Decomposes work into typed tasks, routes each task to the cheapest capable model via OpenRouter, and logs real usage/cost by date + model.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|