@primusclaw/connector 0.1.3 → 0.1.4
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 +3 -2
- package/dist/cli.js +106 -26
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -94,7 +94,8 @@ In Clawy (or any chat client built on Claw), send `/agents`. The agents of this
|
|
|
94
94
|
A session started there runs on this machine, inside one of your `roots`, and each permission request comes back as a card in the chat.
|
|
95
95
|
|
|
96
96
|
To disconnect: `claw-connector pause` (stay offline until `resume`), `claw-connector stop`, or `claw-connector logout` (also
|
|
97
|
-
|
|
97
|
+
revokes this machine's connector at Claw, so it can be sent nothing more, and asks the identity service to revoke the device
|
|
98
|
+
sign-in; a saved key is deleted from this machine, not revoked at Claw).
|
|
98
99
|
|
|
99
100
|
## agents.toml
|
|
100
101
|
|
|
@@ -336,7 +337,7 @@ session stops the agent process; it never deletes the conversation.
|
|
|
336
337
|
| `status [--json]` | Running or not, paused, signed in, agents, adapter processes, the last disconnect and its reason (`last down:`), a `problem:` line when a new sign-in is needed, and a `warning:` line for each agent whose sessions will be refused because Claude's own settings default to `bypassPermissions` (see [Running without per-step approval](#running-without-per-step-approval)). |
|
|
337
338
|
| `pause` / `resume` | Stop every agent within a second and stay offline, or undo that. |
|
|
338
339
|
| `stop` | Stop the running connector and every agent it started. |
|
|
339
|
-
| `logout` | Stop, ask the identity service to revoke the sign-in, delete it from this machine. The
|
|
340
|
+
| `logout` | Stop, revoke this machine's connector at Claw (with a key, `DELETE /v1/remote-agents/connectors/:cid`; with a device sign-in, the connector-side `POST .../connectors/:cid/revoke`), ask the identity service to revoke the sign-in, and delete it from this machine. The next sign-in registers a new connector. When Claw cannot be reached or refuses (a Claw from before the connector-side route refuses a device sign-in), the sign-in is still deleted, but `logout` says the connector was not revoked, prints the `curl` that revokes it with a Claw API key of yours, keeps its id (so a later sign-in reuses it), and exits 1. |
|
|
340
341
|
|
|
341
342
|
**Kill switch.** Every adapter is started in its own process group and recorded (pid and start time) in
|
|
342
343
|
`children.json`. `stop`, `pause`, a revocation and Ctrl-C signal exactly those groups: a process that is not recorded, or
|
package/dist/cli.js
CHANGED
|
@@ -406,6 +406,17 @@ var ClawClient = class {
|
|
|
406
406
|
const d = await this.json("POST", `/v1/remote-agents/connectors/${cid}/events`, { batch_id: batchId, events });
|
|
407
407
|
return { acked: isObject(d.acked) ? d.acked : {}, rejected: Array.isArray(d.rejected) ? d.rejected : [] };
|
|
408
408
|
}
|
|
409
|
+
/**
|
|
410
|
+
* Revoke connector `cid` at Claw, for `logout`: Claw sends it nothing more and closes its sessions. A key is a user's
|
|
411
|
+
* credential and uses the user-side DELETE; a device sign-in's access token is refused there, and uses the
|
|
412
|
+
* connector-side route made for this. Throws {@link HttpError} 404 `not_found` when Claw has no live row by that id
|
|
413
|
+
* for this sign-in (already revoked, or another account's).
|
|
414
|
+
*/
|
|
415
|
+
async revoke(cid, kind) {
|
|
416
|
+
const id = encodeURIComponent(cid);
|
|
417
|
+
if (kind === "key") await this.json("DELETE", `/v1/remote-agents/connectors/${id}`);
|
|
418
|
+
else await this.json("POST", `/v1/remote-agents/connectors/${id}/revoke`);
|
|
419
|
+
}
|
|
409
420
|
/**
|
|
410
421
|
* Hold the command stream. Yields every frame (a `ping` too, since it is
|
|
411
422
|
* proof of life). Ends normally when the server closes it; throws
|
|
@@ -2600,16 +2611,16 @@ var Session = class {
|
|
|
2600
2611
|
* delivers it again instead of losing it.
|
|
2601
2612
|
*/
|
|
2602
2613
|
enqueuePrompt(text, cmdId, started = () => {
|
|
2603
|
-
}) {
|
|
2614
|
+
}, cmdSeq = null) {
|
|
2604
2615
|
if (this.closed) {
|
|
2605
|
-
this.rejectCommand("prompt", cmdId, "session_closed");
|
|
2616
|
+
this.rejectCommand("prompt", cmdId, "session_closed", cmdSeq);
|
|
2606
2617
|
return started();
|
|
2607
2618
|
}
|
|
2608
2619
|
if (this.queue.length >= MAX_QUEUED_PROMPTS) {
|
|
2609
|
-
this.rejectCommand("prompt", cmdId, "too_many_queued_prompts");
|
|
2620
|
+
this.rejectCommand("prompt", cmdId, "too_many_queued_prompts", cmdSeq);
|
|
2610
2621
|
return started();
|
|
2611
2622
|
}
|
|
2612
|
-
this.queue.push({ text, cmdId, started });
|
|
2623
|
+
this.queue.push({ text, cmdId, cmdSeq, started });
|
|
2613
2624
|
if (!this.turnRunning) void this.pump();
|
|
2614
2625
|
}
|
|
2615
2626
|
/** Prompts that will now never run (the process is being stopped): they count as handled. */
|
|
@@ -2624,21 +2635,22 @@ var Session = class {
|
|
|
2624
2635
|
try {
|
|
2625
2636
|
while (this.queue.length && !this.closed) {
|
|
2626
2637
|
const next = this.queue.shift();
|
|
2627
|
-
await this.runTurn(next.text, next.started, next.cmdId);
|
|
2638
|
+
await this.runTurn(next.text, next.started, next.cmdId, next.cmdSeq);
|
|
2628
2639
|
}
|
|
2629
2640
|
} finally {
|
|
2630
2641
|
this.turnRunning = false;
|
|
2631
2642
|
this.touch();
|
|
2632
2643
|
}
|
|
2633
2644
|
}
|
|
2634
|
-
|
|
2645
|
+
/** Every `turn_end` of the turn names its prompt: `cmd_id`, and `cmd_seq` (a number, which no credential scrub ever masks). */
|
|
2646
|
+
async runTurn(text, started, cmdId, cmdSeq) {
|
|
2635
2647
|
if (this.agent.attach) {
|
|
2636
2648
|
const refused = await this.preflight();
|
|
2637
2649
|
if (refused) {
|
|
2638
2650
|
started();
|
|
2639
2651
|
this.audit("prompt_refused", { reason: refused.reason, detail: refused.detail.slice(0, 300) });
|
|
2640
|
-
this.rejectCommand("prompt", cmdId, refused.reason);
|
|
2641
|
-
this.emit("turn_end", { stopReason: "error", error: refused.reason });
|
|
2652
|
+
this.rejectCommand("prompt", cmdId, refused.reason, cmdSeq);
|
|
2653
|
+
this.emit("turn_end", { stopReason: "error", error: refused.reason, cmd_id: cmdId, cmd_seq: cmdSeq });
|
|
2642
2654
|
return;
|
|
2643
2655
|
}
|
|
2644
2656
|
}
|
|
@@ -2646,7 +2658,7 @@ var Session = class {
|
|
|
2646
2658
|
await this.ensureAdapter();
|
|
2647
2659
|
} catch (e) {
|
|
2648
2660
|
started();
|
|
2649
|
-
this.emit("turn_end", { stopReason: "error", error: e instanceof SessionRefusal ? e.reason : "adapter_unavailable" });
|
|
2661
|
+
this.emit("turn_end", { stopReason: "error", error: e instanceof SessionRefusal ? e.reason : "adapter_unavailable", cmd_id: cmdId, cmd_seq: cmdSeq });
|
|
2650
2662
|
return this.refuse(e);
|
|
2651
2663
|
}
|
|
2652
2664
|
const a = this.adapter;
|
|
@@ -2658,10 +2670,10 @@ var Session = class {
|
|
|
2658
2670
|
started();
|
|
2659
2671
|
const res = await reply;
|
|
2660
2672
|
const usage = isObject(res.usage) ? res.usage : void 0;
|
|
2661
|
-
this.emit("turn_end", { stopReason: typeof res.stopReason === "string" ? res.stopReason : "end_turn", ...usage ? { usage } : {} });
|
|
2673
|
+
this.emit("turn_end", { stopReason: typeof res.stopReason === "string" ? res.stopReason : "end_turn", ...usage ? { usage } : {}, cmd_id: cmdId, cmd_seq: cmdSeq });
|
|
2662
2674
|
} catch (e) {
|
|
2663
2675
|
this.audit("turn_failed", { error: errMessage(e) });
|
|
2664
|
-
this.emit("turn_end", { stopReason: "error", error: errMessage(e).slice(0, 300) });
|
|
2676
|
+
this.emit("turn_end", { stopReason: "error", error: errMessage(e).slice(0, 300), cmd_id: cmdId, cmd_seq: cmdSeq });
|
|
2665
2677
|
} finally {
|
|
2666
2678
|
this.markTranscript();
|
|
2667
2679
|
if (watch) {
|
|
@@ -2730,15 +2742,20 @@ var Session = class {
|
|
|
2730
2742
|
this.emit("session_resumed", { acp_session_id: this.acpId, modes: this.shownModes(), currentMode: this.currentMode, unattended: this.unattended });
|
|
2731
2743
|
this.touch();
|
|
2732
2744
|
}
|
|
2733
|
-
/**
|
|
2745
|
+
/**
|
|
2746
|
+
* `cancel`: stop the turn, drop what was queued, answer every waiting permission request `cancelled`. Each prompt dropped
|
|
2747
|
+
* unrun is refused by name (`command_rejected`, reason `cancelled`, with its `cmd_id` and `cmd_seq`): it never gets a `turn_end`.
|
|
2748
|
+
*/
|
|
2734
2749
|
async cancel() {
|
|
2735
|
-
const
|
|
2750
|
+
const dropped = this.queue.map((q) => [q.cmdId, q.cmdSeq]);
|
|
2751
|
+
const queued = dropped.length;
|
|
2736
2752
|
this.dropQueued();
|
|
2753
|
+
for (const [cmdId, cmdSeq] of dropped) this.rejectCommand("prompt", cmdId, "cancelled", cmdSeq);
|
|
2737
2754
|
const wasRunning = this.turnRunning;
|
|
2738
2755
|
if (wasRunning && this.adapter && this.acpId) await this.adapter.notify("session/cancel", { sessionId: this.acpId }).catch(() => void 0);
|
|
2739
2756
|
for (const actionId of this.book.cancelAll()) this.emit("permission_resolved", { actionId, optionId: null, outcome: "cancelled", by: "cancel" });
|
|
2740
2757
|
this.audit("cancelled", { turn_running: wasRunning, queued_dropped: queued });
|
|
2741
|
-
if (!wasRunning) this.emit("turn_end", { stopReason: "cancelled" });
|
|
2758
|
+
if (!wasRunning) this.emit("turn_end", { stopReason: "cancelled", cmd_id: null, cmd_seq: null });
|
|
2742
2759
|
}
|
|
2743
2760
|
// --- ending ----------------------------------------------------------------------------
|
|
2744
2761
|
/** The adapter's process goes away; the session stays and resumes on demand. */
|
|
@@ -2773,9 +2790,10 @@ var Session = class {
|
|
|
2773
2790
|
this.audit("session_refused", { reason: r.reason, message: r.message.slice(0, 300) });
|
|
2774
2791
|
await this.close(r.reason);
|
|
2775
2792
|
}
|
|
2776
|
-
|
|
2793
|
+
/** A command refused; a prompt's refusal names it by `cmd_seq` too, as its turn_end would. */
|
|
2794
|
+
rejectCommand(type, cmdId, reason, cmdSeq = null) {
|
|
2777
2795
|
this.audit("command_rejected", { type, cmd_id: cmdId, reason });
|
|
2778
|
-
this.emit("command_rejected", { type, cmd_id: cmdId, reason });
|
|
2796
|
+
this.emit("command_rejected", { type, cmd_id: cmdId, reason, ...cmdSeq !== null ? { cmd_seq: cmdSeq } : {} });
|
|
2779
2797
|
}
|
|
2780
2798
|
};
|
|
2781
2799
|
function mapAdapterError(e) {
|
|
@@ -2801,6 +2819,7 @@ function limitStrings(value, max) {
|
|
|
2801
2819
|
|
|
2802
2820
|
// src/manager.ts
|
|
2803
2821
|
var MAX_ACTIVE_SESSIONS = 10;
|
|
2822
|
+
var cmdSeqOf = (cmd) => Number.isSafeInteger(cmd.cmd_seq) ? cmd.cmd_seq : null;
|
|
2804
2823
|
var SessionManager = class {
|
|
2805
2824
|
constructor(env) {
|
|
2806
2825
|
this.env = env;
|
|
@@ -2869,10 +2888,10 @@ var SessionManager = class {
|
|
|
2869
2888
|
switch (type) {
|
|
2870
2889
|
case "prompt":
|
|
2871
2890
|
if (typeof cmd.text !== "string" || cmd.text === "") {
|
|
2872
|
-
s.rejectCommand("prompt", cmdId, "text_required");
|
|
2891
|
+
s.rejectCommand("prompt", cmdId, "text_required", cmdSeqOf(cmd));
|
|
2873
2892
|
return false;
|
|
2874
2893
|
}
|
|
2875
|
-
s.enqueuePrompt(cmd.text, cmdId, handled);
|
|
2894
|
+
s.enqueuePrompt(cmd.text, cmdId, handled, cmdSeqOf(cmd));
|
|
2876
2895
|
return true;
|
|
2877
2896
|
case "cancel":
|
|
2878
2897
|
await s.cancel();
|
|
@@ -3640,7 +3659,7 @@ var Connector = class {
|
|
|
3640
3659
|
this.d.audit.record("command_dropped", { reason: "malformed_frame", id: f.id });
|
|
3641
3660
|
return;
|
|
3642
3661
|
}
|
|
3643
|
-
const cmd = data;
|
|
3662
|
+
const cmd = { ...data, cmd_seq: seq };
|
|
3644
3663
|
if (this.ledger.seen(seq, cmd.cmd_id)) return;
|
|
3645
3664
|
this.ledger.begin(seq, cmd.cmd_id);
|
|
3646
3665
|
void this.manager.handle(cmd, () => {
|
|
@@ -4181,7 +4200,7 @@ function status(ctx, d, h, json) {
|
|
|
4181
4200
|
}
|
|
4182
4201
|
|
|
4183
4202
|
// src/version.ts
|
|
4184
|
-
var VERSION = "0.1.
|
|
4203
|
+
var VERSION = "0.1.4";
|
|
4185
4204
|
|
|
4186
4205
|
// src/commands.ts
|
|
4187
4206
|
var EXIT = { ok: 0, failure: 1, usage: 2, needsPerson: 78, revoked: 78 };
|
|
@@ -4210,7 +4229,7 @@ Usage: claw-connector <command> [options]
|
|
|
4210
4229
|
pause stop every agent now and stay offline until "resume"
|
|
4211
4230
|
resume undo "pause"
|
|
4212
4231
|
stop stop the running connector and every agent it started
|
|
4213
|
-
logout stop, revoke the sign-in and delete
|
|
4232
|
+
logout stop, revoke this connector at Claw and the sign-in, and delete the sign-in
|
|
4214
4233
|
--version, --help
|
|
4215
4234
|
|
|
4216
4235
|
Files: ${"$"}XDG_CONFIG_HOME/claw-connector (agents.toml, credentials.json) and ${"$"}XDG_STATE_HOME/claw-connector
|
|
@@ -4607,21 +4626,74 @@ async function cmdStop(ctx, quiet = false) {
|
|
|
4607
4626
|
if (!quiet) ctx.out(pid === null ? `no connector running${left.killed.length ? `; stopped ${left.killed.length} leftover agent process group(s)` : ""}` : `stopped the connector${left.killed.length ? ` and ${left.killed.length} leftover agent process group(s)` : ""}`);
|
|
4608
4627
|
return EXIT.ok;
|
|
4609
4628
|
}
|
|
4629
|
+
function storedConnector(ctx) {
|
|
4630
|
+
try {
|
|
4631
|
+
const raw = JSON.parse(fs16.readFileSync(identityFile(ctx.dirs), "utf8"));
|
|
4632
|
+
if (isObject(raw) && typeof raw.server === "string" && typeof raw.connector_id === "string") {
|
|
4633
|
+
return { server: raw.server, connector_id: raw.connector_id, revoked: raw.revoked === true };
|
|
4634
|
+
}
|
|
4635
|
+
} catch {
|
|
4636
|
+
}
|
|
4637
|
+
return null;
|
|
4638
|
+
}
|
|
4639
|
+
var revokeByHand = (server, cid) => `curl -fsS -X DELETE -H "Authorization: Bearer <your Claw API key>" ${server}/v1/remote-agents/connectors/${encodeURIComponent(cid)}`;
|
|
4610
4640
|
async function cmdLogout(ctx) {
|
|
4611
4641
|
await cmdStop(ctx, true);
|
|
4612
4642
|
const file = credentialsFile(ctx.dirs);
|
|
4613
4643
|
let creds = null;
|
|
4644
|
+
let credsProblem = null;
|
|
4614
4645
|
try {
|
|
4615
4646
|
creds = loadCredentials(file);
|
|
4616
|
-
} catch {
|
|
4647
|
+
} catch (e) {
|
|
4648
|
+
credsProblem = errMessage(e);
|
|
4649
|
+
}
|
|
4650
|
+
const row = storedConnector(ctx);
|
|
4651
|
+
let latest = creds;
|
|
4652
|
+
let notRevoked = null;
|
|
4653
|
+
let rowGone = false;
|
|
4654
|
+
if (row && !row.revoked) {
|
|
4655
|
+
if (!creds) {
|
|
4656
|
+
notRevoked = credsProblem ? `the sign-in on this machine is unreadable (${credsProblem})` : "this machine is not signed in";
|
|
4657
|
+
} else {
|
|
4658
|
+
let server = null;
|
|
4659
|
+
try {
|
|
4660
|
+
let cfg = null;
|
|
4661
|
+
try {
|
|
4662
|
+
cfg = loadCfg(ctx);
|
|
4663
|
+
} catch (e) {
|
|
4664
|
+
if (!(e instanceof ConfigError)) throw e;
|
|
4665
|
+
}
|
|
4666
|
+
server = serverUrl(ctx, void 0, cfg, creds);
|
|
4667
|
+
} catch {
|
|
4668
|
+
}
|
|
4669
|
+
if (server !== row.server) {
|
|
4670
|
+
notRevoked = `it was registered at ${row.server}, but this sign-in is for ${server ?? "no usable server"}, so nothing was sent to either`;
|
|
4671
|
+
} else try {
|
|
4672
|
+
const tokens = providerFor(creds, (c) => {
|
|
4673
|
+
latest = c;
|
|
4674
|
+
saveCredentials(file, c);
|
|
4675
|
+
}, { fetch: ctx.fetch });
|
|
4676
|
+
const client2 = new ClawClient({ server: row.server, tokens, fetch: ctx.fetch, requestTimeoutMs: 15e3 });
|
|
4677
|
+
await client2.revoke(row.connector_id, creds.kind);
|
|
4678
|
+
rowGone = true;
|
|
4679
|
+
ctx.out(`revoked connector ${row.connector_id} at ${row.server}: Claw sends it nothing more`);
|
|
4680
|
+
} catch (e) {
|
|
4681
|
+
if (e instanceof RevokedError || e instanceof HttpError && e.status === 404 && e.code === "not_found") {
|
|
4682
|
+
rowGone = true;
|
|
4683
|
+
ctx.out(`Claw has no live connector ${row.connector_id} for this sign-in (already revoked, or registered by another account)`);
|
|
4684
|
+
} else {
|
|
4685
|
+
notRevoked = errMessage(e);
|
|
4686
|
+
}
|
|
4687
|
+
}
|
|
4688
|
+
}
|
|
4617
4689
|
}
|
|
4618
|
-
if (
|
|
4690
|
+
if (latest?.kind === "device" && latest.identity_url && latest.refresh_token) {
|
|
4619
4691
|
try {
|
|
4620
4692
|
const f = ctx.fetch ?? fetch;
|
|
4621
|
-
await f(`${
|
|
4693
|
+
await f(`${latest.identity_url}/oauth/revoke`, {
|
|
4622
4694
|
method: "POST",
|
|
4623
4695
|
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
4624
|
-
body: new URLSearchParams({ token:
|
|
4696
|
+
body: new URLSearchParams({ token: latest.refresh_token, token_type_hint: "refresh_token", client_id: latest.client_id ?? DEFAULT_CLIENT_ID }),
|
|
4625
4697
|
signal: AbortSignal.timeout(1e4)
|
|
4626
4698
|
});
|
|
4627
4699
|
} catch (e) {
|
|
@@ -4629,7 +4701,15 @@ async function cmdLogout(ctx) {
|
|
|
4629
4701
|
}
|
|
4630
4702
|
}
|
|
4631
4703
|
fs16.rmSync(file, { force: true });
|
|
4632
|
-
|
|
4704
|
+
if (rowGone) fs16.rmSync(identityFile(ctx.dirs), { force: true });
|
|
4705
|
+
else forgetRevokedIdentity(ctx);
|
|
4706
|
+
if (row && notRevoked !== null) {
|
|
4707
|
+
ctx.err(`signed out on this machine, but connector ${row.connector_id} was NOT revoked at Claw (${notRevoked}).`);
|
|
4708
|
+
ctx.err("It is still your connector there, and instructions sent to it wait for the next sign-in on this machine. Revoke it with:");
|
|
4709
|
+
ctx.err(` ${revokeByHand(row.server, row.connector_id)}`);
|
|
4710
|
+
ctx.err("or sign in again and run logout once Claw is reachable.");
|
|
4711
|
+
return EXIT.failure;
|
|
4712
|
+
}
|
|
4633
4713
|
ctx.out("signed out");
|
|
4634
4714
|
return EXIT.ok;
|
|
4635
4715
|
}
|