@tablation/crew 0.67.2 → 0.68.1
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 +133 -2
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -2662,6 +2662,14 @@ function resolveApiKey(r) {
|
|
|
2662
2662
|
`route "${r.route}": no API key (looked for, in order: apiKey; ${r.apiKeyVar ?? "VAR"} in ${r.apiKeyFile ?? "<unset>"}; CREW_API_KEY in the environment)`
|
|
2663
2663
|
);
|
|
2664
2664
|
}
|
|
2665
|
+
function credentialSource(r) {
|
|
2666
|
+
if (r.apiKey) return KEYCHAIN_ELIGIBLE_ROUTES.has(r) ? "keychain" : "inline";
|
|
2667
|
+
if (r.apiKeyFile && r.apiKeyVar && existsSync7(r.apiKeyFile)) {
|
|
2668
|
+
const re = new RegExp(`^${r.apiKeyVar}=`);
|
|
2669
|
+
if (readFileSync6(r.apiKeyFile, "utf8").split("\n").some((l) => re.test(l))) return "apiKeyFile";
|
|
2670
|
+
}
|
|
2671
|
+
return process.env.CREW_API_KEY?.trim() ? "CREW_API_KEY" : "none";
|
|
2672
|
+
}
|
|
2665
2673
|
var KEYCHAIN_ELIGIBLE_ROUTES = /* @__PURE__ */ new WeakSet();
|
|
2666
2674
|
async function hydrateApiKeys(routes, getStore = getSessionStore) {
|
|
2667
2675
|
let store;
|
|
@@ -3879,6 +3887,9 @@ function ticketMentions(text) {
|
|
|
3879
3887
|
var TicketAttributionTracker = class {
|
|
3880
3888
|
#tokensByTicket = /* @__PURE__ */ new Map();
|
|
3881
3889
|
#current;
|
|
3890
|
+
/** Tracker record uuid → ticket key, learned when a write names both (a claim resolves the key from the last mention). */
|
|
3891
|
+
#keyByRecord = /* @__PURE__ */ new Map();
|
|
3892
|
+
#lastBy = {};
|
|
3882
3893
|
/** The ticket key most recently named in the transcript, or undefined before any. */
|
|
3883
3894
|
get current() {
|
|
3884
3895
|
return this.#current;
|
|
@@ -3895,6 +3906,37 @@ var TicketAttributionTracker = class {
|
|
|
3895
3906
|
if (this.#current === void 0 || tokens <= 0) return;
|
|
3896
3907
|
this.#tokensByTicket.set(this.#current, (this.#tokensByTicket.get(this.#current) ?? 0) + tokens);
|
|
3897
3908
|
}
|
|
3909
|
+
/**
|
|
3910
|
+
* Record a tool call if it is a ticket-targeted tracker write (claim,
|
|
3911
|
+
* hand-off or comment). The ticket key comes from a record uuid seen on an
|
|
3912
|
+
* earlier write when known, else from the key last named in the transcript
|
|
3913
|
+
* (a claim or hand-off announces its ticket). A comment on a record never
|
|
3914
|
+
* seen before is ignored: its uuid cannot be resolved to a key.
|
|
3915
|
+
*/
|
|
3916
|
+
noteWrite(tool, input) {
|
|
3917
|
+
const w = classifyWrite(tool, input);
|
|
3918
|
+
if (!w) return;
|
|
3919
|
+
let key = w.recordId ? this.#keyByRecord.get(w.recordId) : void 0;
|
|
3920
|
+
if (w.kind !== "comment") key ??= this.#current;
|
|
3921
|
+
if (!key) return;
|
|
3922
|
+
if (w.kind !== "comment" && w.recordId) this.#keyByRecord.set(w.recordId, key);
|
|
3923
|
+
this.#lastBy[w.kind] = key;
|
|
3924
|
+
}
|
|
3925
|
+
/**
|
|
3926
|
+
* How the attribution is decided: a hand-off write beats a claim, a claim
|
|
3927
|
+
* beats a comment (so a follow-up ticket filed or commented on while
|
|
3928
|
+
* working never displaces the claimed one), and any write beats the
|
|
3929
|
+
* mention-token heuristic, which is only the fallback for runs that made
|
|
3930
|
+
* no ticket-targeted write. The pre-run hint is last.
|
|
3931
|
+
*/
|
|
3932
|
+
decide(hint) {
|
|
3933
|
+
for (const by of ["handoff", "claim", "comment"]) {
|
|
3934
|
+
const ticket = this.#lastBy[by];
|
|
3935
|
+
if (ticket) return { ticket, by };
|
|
3936
|
+
}
|
|
3937
|
+
if (this.#tokensByTicket.size > 0) return { ticket: this.winner(hint), by: "mentions" };
|
|
3938
|
+
return { ticket: hint, by: "hint" };
|
|
3939
|
+
}
|
|
3898
3940
|
/**
|
|
3899
3941
|
* The ticket key the run spent the most tokens on, or `fallback` (the
|
|
3900
3942
|
* pre-run poll hint) when the transcript never named one — an
|
|
@@ -3918,6 +3960,20 @@ function isClaimWrite(tool, input) {
|
|
|
3918
3960
|
const body = input?.body;
|
|
3919
3961
|
return typeof body === "object" && body !== null && body.status === "in_progress";
|
|
3920
3962
|
}
|
|
3963
|
+
var HANDOFF_STATUSES = /* @__PURE__ */ new Set(["fixed", "verified", "needs_info", "accepted"]);
|
|
3964
|
+
function classifyWrite(tool, input) {
|
|
3965
|
+
if (!tool) return void 0;
|
|
3966
|
+
const body = input?.body;
|
|
3967
|
+
if (typeof body !== "object" || body === null) return void 0;
|
|
3968
|
+
const b = body;
|
|
3969
|
+
if (/records_controller_update$/.test(tool)) {
|
|
3970
|
+
const recordId = typeof input?.recordId === "string" ? input.recordId : void 0;
|
|
3971
|
+
if (b.status === "in_progress" || b.status === "qa") return { kind: "claim", recordId };
|
|
3972
|
+
if (typeof b.status === "string" && HANDOFF_STATUSES.has(b.status)) return { kind: "handoff", recordId };
|
|
3973
|
+
}
|
|
3974
|
+
if (/records_controller_create$/.test(tool) && typeof b.ticket_id === "string") return { kind: "comment", recordId: b.ticket_id };
|
|
3975
|
+
return void 0;
|
|
3976
|
+
}
|
|
3921
3977
|
|
|
3922
3978
|
// src/agent.ts
|
|
3923
3979
|
var DIGEST_MAX_AGE_SECONDS = 600;
|
|
@@ -4208,6 +4264,7 @@ var RunObserver = class {
|
|
|
4208
4264
|
let turnText = "";
|
|
4209
4265
|
for (const ev of events) if ((ev.kind === "thought" || ev.kind === "text") && ev.text) this.attribution.note(ev.text);
|
|
4210
4266
|
for (const ev of events) {
|
|
4267
|
+
if (ev.kind === "tool") this.attribution.noteWrite(ev.tool, ev.input);
|
|
4211
4268
|
if (ev.kind === "tool" && isClaimWrite(ev.tool, ev.input)) {
|
|
4212
4269
|
const claimed = this.attribution.current;
|
|
4213
4270
|
if (claimed && claimed !== this.liveTicket) {
|
|
@@ -4292,12 +4349,13 @@ function writeRunMeta(id, startedAt, liveTicket, extra) {
|
|
|
4292
4349
|
}
|
|
4293
4350
|
}
|
|
4294
4351
|
async function finishRun(id, obs, emit, startedAt, end, agentLogKey) {
|
|
4295
|
-
const attributedTicket = obs.attribution.
|
|
4352
|
+
const { ticket: attributedTicket, by: attributedBy } = obs.attribution.decide(obs.liveTicket);
|
|
4296
4353
|
const identity = { toolchain: id.toolchain, harness: id.harness, provider: id.provider };
|
|
4297
4354
|
const { result } = obs;
|
|
4298
4355
|
try {
|
|
4299
4356
|
writeRunMeta(id, startedAt, obs.liveTicket, {
|
|
4300
4357
|
attributedTicket: attributedTicket ?? null,
|
|
4358
|
+
attributedBy,
|
|
4301
4359
|
running: void 0,
|
|
4302
4360
|
exitCode: end.code,
|
|
4303
4361
|
...end.interrupted ? { interrupted: end.interrupted } : {},
|
|
@@ -11674,6 +11732,61 @@ function formatProjectsReport(report) {
|
|
|
11674
11732
|
`;
|
|
11675
11733
|
}
|
|
11676
11734
|
|
|
11735
|
+
// src/whoami.ts
|
|
11736
|
+
async function buildWhoami(routes, deps = {}) {
|
|
11737
|
+
const doFetch = deps.fetch ?? fetch;
|
|
11738
|
+
return Promise.all(routes.map(async (route) => {
|
|
11739
|
+
const credential = credentialSource(route);
|
|
11740
|
+
try {
|
|
11741
|
+
const key = resolveApiKey(route);
|
|
11742
|
+
const q = route.resolved ? `?workspaceId=${encodeURIComponent(route.resolved.workspaceId)}` : "";
|
|
11743
|
+
const res = await doFetch(`${route.baseUrl.replace(/\/+$/, "")}/api/auth/me${q}`, {
|
|
11744
|
+
headers: { Authorization: `Bearer ${key}`, "User-Agent": deps.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
|
|
11745
|
+
});
|
|
11746
|
+
if (res.status === 401 || res.status === 403) {
|
|
11747
|
+
return { route: route.route, ok: false, credential, error: `key rejected by ${route.baseUrl} (HTTP ${res.status}): expired or revoked, re-run crew connect` };
|
|
11748
|
+
}
|
|
11749
|
+
if (!res.ok) return { route: route.route, ok: false, credential, error: `/auth/me answered HTTP ${res.status}` };
|
|
11750
|
+
const me = await res.json();
|
|
11751
|
+
const operatorId = route.resolved?.operator ?? null;
|
|
11752
|
+
let operatorName = null;
|
|
11753
|
+
if (operatorId && deps.operatorName) {
|
|
11754
|
+
try {
|
|
11755
|
+
operatorName = await deps.operatorName(route, operatorId) ?? null;
|
|
11756
|
+
} catch {
|
|
11757
|
+
}
|
|
11758
|
+
}
|
|
11759
|
+
return {
|
|
11760
|
+
route: route.route,
|
|
11761
|
+
ok: true,
|
|
11762
|
+
credential,
|
|
11763
|
+
identity: { id: me.id ?? null, email: me.email ?? null, name: me.name ?? null },
|
|
11764
|
+
role: typeof me.role === "string" ? me.role : null,
|
|
11765
|
+
operator: { id: operatorId, name: operatorName }
|
|
11766
|
+
};
|
|
11767
|
+
} catch (e) {
|
|
11768
|
+
return { route: route.route, ok: false, credential, error: e.message };
|
|
11769
|
+
}
|
|
11770
|
+
}));
|
|
11771
|
+
}
|
|
11772
|
+
function formatWhoami(report) {
|
|
11773
|
+
const lines = [];
|
|
11774
|
+
for (const r of report) {
|
|
11775
|
+
lines.push(`route ${r.route}`);
|
|
11776
|
+
if (!r.ok) {
|
|
11777
|
+
lines.push(` error: ${r.error}`, ` credential: ${r.credential}`);
|
|
11778
|
+
continue;
|
|
11779
|
+
}
|
|
11780
|
+
const who2 = [r.identity.name, r.identity.email ? `<${r.identity.email}>` : null].filter(Boolean).join(" ");
|
|
11781
|
+
lines.push(` signed in as: ${who2 || r.identity.id || "(unknown)"}${r.identity.id ? ` (${r.identity.id})` : ""}`);
|
|
11782
|
+
lines.push(` role: ${r.role ?? "(unknown)"}`);
|
|
11783
|
+
lines.push(` operator: ${r.operator.id ? `${r.operator.name ?? "(unnamed)"} (${r.operator.id})` : "(not linked)"}`);
|
|
11784
|
+
lines.push(` credential: ${r.credential}`);
|
|
11785
|
+
}
|
|
11786
|
+
return `${lines.join("\n")}
|
|
11787
|
+
`;
|
|
11788
|
+
}
|
|
11789
|
+
|
|
11677
11790
|
// src/cli.ts
|
|
11678
11791
|
import { StaleWriteError as StaleWriteError9 } from "@tablation/client";
|
|
11679
11792
|
|
|
@@ -15477,6 +15590,7 @@ function usage() {
|
|
|
15477
15590
|
crew watch [route] live view of what the crew is doing
|
|
15478
15591
|
crew status [route] [--json] paused/running state; --json for a machine reader
|
|
15479
15592
|
crew projects [--route R] [--json] the Projects rows each route can read (id, name, prefix, work mode)
|
|
15593
|
+
crew whoami [route] [--json] the signed-in identity, key role, operator Crew row and credential source per route
|
|
15480
15594
|
crew reload make the running daemon re-read crew.yaml (--now: restart it immediately)
|
|
15481
15595
|
crew doctor [route] [--fix] [--offline] [--json] preflight; --fix enables clean routes and offers crew install;
|
|
15482
15596
|
--offline skips push probes; --json prints push-auth and tool checks only
|
|
@@ -15698,7 +15812,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
15698
15812
|
const DAEMON_SUBCOMMANDS = /* @__PURE__ */ new Set(["start", "stop", "status", "restart"]);
|
|
15699
15813
|
const worktreeArgs = command === "worktree" ? splitWorktreeArgs(positional, cfg.routes.map((r) => r.route)) : void 0;
|
|
15700
15814
|
const named = worktreeArgs ? worktreeArgs.route : command === "daemon" && DAEMON_SUBCOMMANDS.has(positional[1] ?? "") ? positional[2] : positional[1];
|
|
15701
|
-
const fleetWide = command === "inbox" || command === "connect" || command === "disconnect" || command === "agents" || command === "skills" || command === "logbook" || command === "repos" || command === "projects" || releaseFleetWide || command === "pause" || command === "resume" || command === "reload" || FLEET_CAPABLE.has(command) && !named && cfg.routes.length > 1 || worktreeArgs !== void 0 && !named && cfg.routes.length > 1;
|
|
15815
|
+
const fleetWide = command === "inbox" || command === "connect" || command === "disconnect" || command === "agents" || command === "skills" || command === "logbook" || command === "repos" || command === "projects" || command === "whoami" || releaseFleetWide || command === "pause" || command === "resume" || command === "reload" || FLEET_CAPABLE.has(command) && !named && cfg.routes.length > 1 || worktreeArgs !== void 0 && !named && cfg.routes.length > 1;
|
|
15702
15816
|
let route;
|
|
15703
15817
|
try {
|
|
15704
15818
|
route = fleetWide ? cfg.routes[0] : findRoute(cfg, named);
|
|
@@ -18902,6 +19016,23 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
18902
19016
|
if (results.some((r) => !r.ok)) process.exitCode = 1;
|
|
18903
19017
|
break;
|
|
18904
19018
|
}
|
|
19019
|
+
case "whoami": {
|
|
19020
|
+
const only = value("route") ?? positional[1];
|
|
19021
|
+
const wanted = only ? cfg.routes.filter((r) => r.route === only) : cfg.routes;
|
|
19022
|
+
if (only && wanted.length === 0) {
|
|
19023
|
+
process.stderr.write(`crew whoami: no route "${only}" (have: ${cfg.routes.map((r) => r.route).join(", ")})
|
|
19024
|
+
`);
|
|
19025
|
+
process.exit(2);
|
|
19026
|
+
}
|
|
19027
|
+
const report = await buildWhoami(wanted, {
|
|
19028
|
+
userAgent: cfg.ship.userAgent,
|
|
19029
|
+
operatorName: async (r, id) => (await new Tracker(r, cfg.ship).crewRows()).find((c) => c.id === id)?.name
|
|
19030
|
+
});
|
|
19031
|
+
process.stdout.write(flag("json") ? `${JSON.stringify(report.length === 1 && only ? report[0] : report, null, 2)}
|
|
19032
|
+
` : formatWhoami(report));
|
|
19033
|
+
if (report.some((r) => !r.ok)) process.exitCode = 1;
|
|
19034
|
+
break;
|
|
19035
|
+
}
|
|
18905
19036
|
case "projects": {
|
|
18906
19037
|
const only = value("route") ?? positional[1];
|
|
18907
19038
|
const wanted = only ? cfg.routes.filter((r) => r.route === only) : cfg.routes;
|
package/package.json
CHANGED