alink-cli 0.6.1 → 0.6.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/daemon.js +273 -297
- package/package.json +4 -2
package/dist/daemon.js
CHANGED
|
@@ -4899,6 +4899,7 @@ var claude = {
|
|
|
4899
4899
|
// configured with (settings.json / ~/.claude.json), merged ahead of these.
|
|
4900
4900
|
models: ["opus", "sonnet", "haiku"],
|
|
4901
4901
|
probeModels: async () => probeClaudeModels(),
|
|
4902
|
+
command: "claude",
|
|
4902
4903
|
acp: { bin: "claude-code-acp", args: [] }
|
|
4903
4904
|
};
|
|
4904
4905
|
|
|
@@ -4928,6 +4929,7 @@ var codex = {
|
|
|
4928
4929
|
// (incl. custom/proxy models), merged ahead of these aliases.
|
|
4929
4930
|
models: ["gpt-5-codex", "gpt-5", "o3"],
|
|
4930
4931
|
probeModels: async () => probeCodexModels(),
|
|
4932
|
+
command: "codex",
|
|
4931
4933
|
acp: { bin: "codex-acp", args: [] }
|
|
4932
4934
|
};
|
|
4933
4935
|
|
|
@@ -4962,7 +4964,8 @@ var REGISTRY = [claude, codex, gemini, qwen, kimi];
|
|
|
4962
4964
|
function detectAgents() {
|
|
4963
4965
|
return REGISTRY.flatMap((def) => {
|
|
4964
4966
|
const bin = resolveBin(def.acp.bin);
|
|
4965
|
-
|
|
4967
|
+
const command = def.command ? resolveBin(def.command) : bin;
|
|
4968
|
+
return bin && command ? [{ ...def, bin }] : [];
|
|
4966
4969
|
});
|
|
4967
4970
|
}
|
|
4968
4971
|
function getAgent(id) {
|
|
@@ -23489,76 +23492,9 @@ var RUNS_DIR = join4(STATE_DIR, "runs");
|
|
|
23489
23492
|
var CONVS_FILE = join4(STATE_DIR, "conversations.json");
|
|
23490
23493
|
var ENCKEYS_FILE = join4(STATE_DIR, "enckeys.json");
|
|
23491
23494
|
var MAX_RUN_BYTES = 512 * 1024;
|
|
23492
|
-
var MAX_RUNS_PER_CONVERSATION = 100;
|
|
23493
|
-
function isValidConversationId(id) {
|
|
23494
|
-
return typeof id === "string" && /^[A-Za-z0-9_-]{1,64}$/.test(id);
|
|
23495
|
-
}
|
|
23496
23495
|
function ensureDirs() {
|
|
23497
23496
|
mkdirSync(RUNS_DIR, { recursive: true });
|
|
23498
23497
|
}
|
|
23499
|
-
function loadConversations() {
|
|
23500
|
-
try {
|
|
23501
|
-
const parsed = JSON.parse(readFileSync3(CONVS_FILE, "utf-8"));
|
|
23502
|
-
return Array.isArray(parsed) ? parsed.filter((c) => isValidConversationId(c?.id)) : [];
|
|
23503
|
-
} catch {
|
|
23504
|
-
return [];
|
|
23505
|
-
}
|
|
23506
|
-
}
|
|
23507
|
-
function saveConversations(convs) {
|
|
23508
|
-
ensureDirs();
|
|
23509
|
-
const tmp = CONVS_FILE + ".tmp";
|
|
23510
|
-
writeFileSync(tmp, JSON.stringify(convs, null, 1));
|
|
23511
|
-
renameSync(tmp, CONVS_FILE);
|
|
23512
|
-
}
|
|
23513
|
-
function listConversations() {
|
|
23514
|
-
return loadConversations().sort((a, b) => b.lastActiveAt - a.lastActiveAt);
|
|
23515
|
-
}
|
|
23516
|
-
function putConversation(patch) {
|
|
23517
|
-
if (!isValidConversationId(patch?.id)) return { error: "conversation.id is missing or malformed" };
|
|
23518
|
-
const strOrUndef = (v) => typeof v === "string" ? v : void 0;
|
|
23519
|
-
const numOrUndef = (v) => typeof v === "number" && Number.isFinite(v) ? v : void 0;
|
|
23520
|
-
const convs = loadConversations();
|
|
23521
|
-
const existing = convs.find((c) => c.id === patch.id);
|
|
23522
|
-
const merged = {
|
|
23523
|
-
id: patch.id,
|
|
23524
|
-
agent: strOrUndef(patch.agent) ?? existing?.agent ?? "",
|
|
23525
|
-
dir: strOrUndef(patch.dir) ?? existing?.dir ?? "",
|
|
23526
|
-
title: strOrUndef(patch.title) ?? existing?.title ?? "",
|
|
23527
|
-
createdAt: numOrUndef(patch.createdAt) ?? existing?.createdAt ?? 0,
|
|
23528
|
-
lastActiveAt: numOrUndef(patch.lastActiveAt) ?? existing?.lastActiveAt ?? 0
|
|
23529
|
-
};
|
|
23530
|
-
const model = patch.model === "" ? void 0 : strOrUndef(patch.model) ?? existing?.model;
|
|
23531
|
-
if (model !== void 0) merged.model = model;
|
|
23532
|
-
const sessionId = patch.sessionId === "" ? void 0 : strOrUndef(patch.sessionId) ?? existing?.sessionId;
|
|
23533
|
-
if (sessionId !== void 0) merged.sessionId = sessionId;
|
|
23534
|
-
const archived = typeof patch.archived === "boolean" ? patch.archived : existing?.archived;
|
|
23535
|
-
if (archived === true) merged.archived = true;
|
|
23536
|
-
const takeover = typeof patch.takeover === "boolean" ? patch.takeover : existing?.takeover;
|
|
23537
|
-
if (takeover === true) merged.takeover = true;
|
|
23538
|
-
if (!merged.agent || !merged.dir || !merged.title || !merged.createdAt || !merged.lastActiveAt) {
|
|
23539
|
-
return { error: "creating a conversation requires agent, dir, title, createdAt and lastActiveAt" };
|
|
23540
|
-
}
|
|
23541
|
-
const next = existing ? convs.map((c) => c.id === merged.id ? merged : c) : [...convs, merged];
|
|
23542
|
-
saveConversations(next);
|
|
23543
|
-
return { conversation: merged };
|
|
23544
|
-
}
|
|
23545
|
-
function deleteConversation(id) {
|
|
23546
|
-
if (!isValidConversationId(id)) return;
|
|
23547
|
-
saveConversations(loadConversations().filter((c) => c.id !== id));
|
|
23548
|
-
rmSync(join4(RUNS_DIR, id), { recursive: true, force: true });
|
|
23549
|
-
}
|
|
23550
|
-
function clearConversations() {
|
|
23551
|
-
rmSync(CONVS_FILE, { force: true });
|
|
23552
|
-
rmSync(RUNS_DIR, { recursive: true, force: true });
|
|
23553
|
-
}
|
|
23554
|
-
function touchConversation(id, patch) {
|
|
23555
|
-
const convs = loadConversations();
|
|
23556
|
-
const conv = convs.find((c) => c.id === id);
|
|
23557
|
-
if (!conv) return;
|
|
23558
|
-
if (patch.lastActiveAt) conv.lastActiveAt = patch.lastActiveAt;
|
|
23559
|
-
if (patch.sessionId) conv.sessionId = patch.sessionId;
|
|
23560
|
-
saveConversations(convs);
|
|
23561
|
-
}
|
|
23562
23498
|
function saveEnckey(machineId, enckey) {
|
|
23563
23499
|
try {
|
|
23564
23500
|
ensureDirs();
|
|
@@ -23578,112 +23514,18 @@ function saveEnckey(machineId, enckey) {
|
|
|
23578
23514
|
return false;
|
|
23579
23515
|
}
|
|
23580
23516
|
}
|
|
23581
|
-
var openRuns = /* @__PURE__ */ new Set();
|
|
23582
|
-
function recordRun(conversationId, meta3) {
|
|
23583
|
-
if (!isValidConversationId(conversationId)) return null;
|
|
23584
|
-
const dir = join4(RUNS_DIR, conversationId);
|
|
23585
|
-
const startedAt = Date.now();
|
|
23586
|
-
const file2 = join4(dir, `${startedAt}-${meta3.runId}.jsonl`);
|
|
23587
|
-
try {
|
|
23588
|
-
mkdirSync(dir, { recursive: true });
|
|
23589
|
-
const files = readdirSync2(dir).filter((f) => f.endsWith(".jsonl")).sort();
|
|
23590
|
-
for (const old of files.slice(0, Math.max(0, files.length - (MAX_RUNS_PER_CONVERSATION - 1)))) {
|
|
23591
|
-
rmSync(join4(dir, old), { force: true });
|
|
23592
|
-
}
|
|
23593
|
-
appendFileSync(file2, JSON.stringify({ kind: "meta", startedAt, ...meta3 }) + "\n");
|
|
23594
|
-
} catch {
|
|
23595
|
-
return null;
|
|
23596
|
-
}
|
|
23597
|
-
openRuns.add(meta3.runId);
|
|
23598
|
-
touchConversation(conversationId, { lastActiveAt: startedAt });
|
|
23599
|
-
let bytes = 0;
|
|
23600
|
-
let truncated = false;
|
|
23601
|
-
const append = (line) => {
|
|
23602
|
-
try {
|
|
23603
|
-
appendFileSync(file2, JSON.stringify(line) + "\n");
|
|
23604
|
-
} catch {
|
|
23605
|
-
}
|
|
23606
|
-
};
|
|
23607
|
-
return {
|
|
23608
|
-
event(e, stored) {
|
|
23609
|
-
if (e.type === "session") touchConversation(conversationId, { sessionId: e.id });
|
|
23610
|
-
if (truncated) return;
|
|
23611
|
-
const line = JSON.stringify({ kind: "event", event: stored ?? e }) + "\n";
|
|
23612
|
-
bytes += line.length;
|
|
23613
|
-
if (bytes > MAX_RUN_BYTES) {
|
|
23614
|
-
truncated = true;
|
|
23615
|
-
append({ kind: "truncated" });
|
|
23616
|
-
return;
|
|
23617
|
-
}
|
|
23618
|
-
try {
|
|
23619
|
-
appendFileSync(file2, line);
|
|
23620
|
-
} catch {
|
|
23621
|
-
}
|
|
23622
|
-
},
|
|
23623
|
-
done(code, error51) {
|
|
23624
|
-
openRuns.delete(meta3.runId);
|
|
23625
|
-
append({ kind: "done", code, ...error51 !== void 0 ? { error: error51 } : {} });
|
|
23626
|
-
touchConversation(conversationId, { lastActiveAt: Date.now() });
|
|
23627
|
-
}
|
|
23628
|
-
};
|
|
23629
|
-
}
|
|
23630
|
-
function history(conversationId) {
|
|
23631
|
-
if (!isValidConversationId(conversationId)) return [];
|
|
23632
|
-
const dir = join4(RUNS_DIR, conversationId);
|
|
23633
|
-
if (!existsSync2(dir)) return [];
|
|
23634
|
-
const runs = [];
|
|
23635
|
-
for (const name of readdirSync2(dir).filter((f) => f.endsWith(".jsonl")).sort()) {
|
|
23636
|
-
let run = null;
|
|
23637
|
-
let text;
|
|
23638
|
-
try {
|
|
23639
|
-
text = readFileSync3(join4(dir, name), "utf-8");
|
|
23640
|
-
} catch {
|
|
23641
|
-
continue;
|
|
23642
|
-
}
|
|
23643
|
-
for (const raw of text.split("\n")) {
|
|
23644
|
-
if (!raw.trim()) continue;
|
|
23645
|
-
let line;
|
|
23646
|
-
try {
|
|
23647
|
-
line = JSON.parse(raw);
|
|
23648
|
-
} catch {
|
|
23649
|
-
continue;
|
|
23650
|
-
}
|
|
23651
|
-
if (line.kind === "meta") {
|
|
23652
|
-
run = {
|
|
23653
|
-
runId: line.runId,
|
|
23654
|
-
agent: line.agent,
|
|
23655
|
-
prompt: line.prompt,
|
|
23656
|
-
...line.cwd !== void 0 ? { cwd: line.cwd } : {},
|
|
23657
|
-
...line.model !== void 0 ? { model: line.model } : {},
|
|
23658
|
-
startedAt: line.startedAt,
|
|
23659
|
-
status: "disconnected",
|
|
23660
|
-
// upgraded below by done / openRuns
|
|
23661
|
-
truncated: false,
|
|
23662
|
-
events: []
|
|
23663
|
-
};
|
|
23664
|
-
} else if (run && line.kind === "event") {
|
|
23665
|
-
if ("type" in line.event && line.event.type === "session") run.sessionId = line.event.id;
|
|
23666
|
-
run.events.push(line.event);
|
|
23667
|
-
} else if (run && line.kind === "truncated") {
|
|
23668
|
-
run.truncated = true;
|
|
23669
|
-
} else if (run && line.kind === "done") {
|
|
23670
|
-
run.status = "done";
|
|
23671
|
-
run.code = line.code;
|
|
23672
|
-
if (line.error !== void 0) run.error = line.error;
|
|
23673
|
-
}
|
|
23674
|
-
}
|
|
23675
|
-
if (!run) continue;
|
|
23676
|
-
if (run.status !== "done" && openRuns.has(run.runId)) run.status = "running";
|
|
23677
|
-
if (run.status === "disconnected") run.error = "daemon disconnected";
|
|
23678
|
-
runs.push(run);
|
|
23679
|
-
}
|
|
23680
|
-
return runs;
|
|
23681
|
-
}
|
|
23682
23517
|
|
|
23683
23518
|
// src/sessions.ts
|
|
23684
|
-
import {
|
|
23519
|
+
import {
|
|
23520
|
+
closeSync,
|
|
23521
|
+
openSync,
|
|
23522
|
+
readFileSync as readFileSync4,
|
|
23523
|
+
readSync,
|
|
23524
|
+
readdirSync as readdirSync3,
|
|
23525
|
+
statSync
|
|
23526
|
+
} from "node:fs";
|
|
23685
23527
|
import { homedir as homedir5 } from "node:os";
|
|
23686
|
-
import { join as join5 } from "node:path";
|
|
23528
|
+
import { basename, join as join5 } from "node:path";
|
|
23687
23529
|
|
|
23688
23530
|
// ../core/src/session-archive.ts
|
|
23689
23531
|
function resultText(content) {
|
|
@@ -23870,122 +23712,257 @@ function firstLine(s) {
|
|
|
23870
23712
|
const line = s.split("\n").find((l) => l.trim()) ?? "";
|
|
23871
23713
|
return line.trim();
|
|
23872
23714
|
}
|
|
23873
|
-
|
|
23874
|
-
|
|
23875
|
-
function claudeProjectsDir() {
|
|
23876
|
-
const base = process.env.AGENTLINK_CLAUDE_HOME || join5(homedir5(), ".claude");
|
|
23877
|
-
return join5(base, "projects");
|
|
23878
|
-
}
|
|
23879
|
-
var META_SLICE_BYTES = 64 * 1024;
|
|
23880
|
-
var MAX_SESSIONS = 40;
|
|
23881
|
-
var MAX_HISTORY_TURNS = 40;
|
|
23882
|
-
var SESSION_ID_RE = /^[A-Za-z0-9_-]{1,128}$/;
|
|
23883
|
-
function readHeadTail(path, size) {
|
|
23884
|
-
if (size <= META_SLICE_BYTES * 2) {
|
|
23885
|
-
const whole = readFileSync4(path, "utf-8");
|
|
23886
|
-
return { head: whole, tail: whole };
|
|
23887
|
-
}
|
|
23888
|
-
const fd = openSync(path, "r");
|
|
23889
|
-
try {
|
|
23890
|
-
const headBuf = Buffer.alloc(META_SLICE_BYTES);
|
|
23891
|
-
readSync(fd, headBuf, 0, META_SLICE_BYTES, 0);
|
|
23892
|
-
const tailBuf = Buffer.alloc(META_SLICE_BYTES);
|
|
23893
|
-
readSync(fd, tailBuf, 0, META_SLICE_BYTES, size - META_SLICE_BYTES);
|
|
23894
|
-
const tail = tailBuf.toString("utf-8");
|
|
23895
|
-
return { head: headBuf.toString("utf-8"), tail: tail.slice(tail.indexOf("\n") + 1) };
|
|
23896
|
-
} finally {
|
|
23897
|
-
closeSync(fd);
|
|
23898
|
-
}
|
|
23715
|
+
function codexPayload(obj) {
|
|
23716
|
+
return obj.payload && typeof obj.payload === "object" ? obj.payload : null;
|
|
23899
23717
|
}
|
|
23900
|
-
function
|
|
23901
|
-
|
|
23902
|
-
|
|
23903
|
-
|
|
23904
|
-
|
|
23905
|
-
|
|
23906
|
-
|
|
23907
|
-
|
|
23908
|
-
|
|
23909
|
-
|
|
23910
|
-
|
|
23718
|
+
function codexMessage(payload) {
|
|
23719
|
+
return typeof payload.message === "string" ? payload.message : "";
|
|
23720
|
+
}
|
|
23721
|
+
function parseCodexArchive(text, opts) {
|
|
23722
|
+
const maxTurns = opts?.maxTurns ?? Infinity;
|
|
23723
|
+
const turns = [];
|
|
23724
|
+
let current = null;
|
|
23725
|
+
let sessionId;
|
|
23726
|
+
let cwd;
|
|
23727
|
+
let omitted = 0;
|
|
23728
|
+
const openTurn = (prompt, startedAt) => {
|
|
23729
|
+
current = { prompt, events: [], ...startedAt !== void 0 ? { startedAt } : {} };
|
|
23730
|
+
turns.push(current);
|
|
23731
|
+
};
|
|
23732
|
+
for (const raw of text.split("\n")) {
|
|
23733
|
+
if (!raw.trim()) continue;
|
|
23734
|
+
let obj;
|
|
23911
23735
|
try {
|
|
23912
|
-
|
|
23736
|
+
obj = JSON.parse(raw);
|
|
23913
23737
|
} catch {
|
|
23738
|
+
omitted++;
|
|
23914
23739
|
continue;
|
|
23915
23740
|
}
|
|
23916
|
-
|
|
23917
|
-
|
|
23918
|
-
|
|
23919
|
-
|
|
23920
|
-
|
|
23921
|
-
|
|
23922
|
-
|
|
23741
|
+
const payload = codexPayload(obj);
|
|
23742
|
+
if (!payload) continue;
|
|
23743
|
+
if (obj.type === "session_meta") {
|
|
23744
|
+
if (!sessionId && typeof payload.id === "string") sessionId = payload.id;
|
|
23745
|
+
if (!cwd && typeof payload.cwd === "string") cwd = payload.cwd;
|
|
23746
|
+
continue;
|
|
23747
|
+
}
|
|
23748
|
+
if (obj.type === "event_msg" && payload.type === "user_message") {
|
|
23749
|
+
const prompt = codexMessage(payload);
|
|
23750
|
+
if (prompt.trim()) openTurn(prompt, tsOf(obj));
|
|
23751
|
+
continue;
|
|
23752
|
+
}
|
|
23753
|
+
if (obj.type === "event_msg" && payload.type === "agent_message") {
|
|
23754
|
+
const message = codexMessage(payload);
|
|
23755
|
+
if (message) {
|
|
23756
|
+
if (!current) openTurn("", tsOf(obj));
|
|
23757
|
+
current.events.push({ type: "text", text: message });
|
|
23758
|
+
}
|
|
23759
|
+
continue;
|
|
23760
|
+
}
|
|
23761
|
+
if (obj.type !== "response_item") continue;
|
|
23762
|
+
const itemType = payload.type;
|
|
23763
|
+
if (itemType === "custom_tool_call" || itemType === "function_call") {
|
|
23764
|
+
if (!current) openTurn("", tsOf(obj));
|
|
23765
|
+
let input = payload.input ?? null;
|
|
23766
|
+
if (typeof input === "string") {
|
|
23767
|
+
try {
|
|
23768
|
+
input = JSON.parse(input);
|
|
23769
|
+
} catch {
|
|
23770
|
+
}
|
|
23923
23771
|
}
|
|
23772
|
+
current.events.push({
|
|
23773
|
+
type: "tool_use",
|
|
23774
|
+
id: String(payload.call_id ?? payload.id ?? ""),
|
|
23775
|
+
name: String(payload.name ?? "tool"),
|
|
23776
|
+
input
|
|
23777
|
+
});
|
|
23778
|
+
} else if (itemType === "custom_tool_call_output" || itemType === "function_call_output") {
|
|
23779
|
+
if (!current) openTurn("", tsOf(obj));
|
|
23780
|
+
current.events.push({
|
|
23781
|
+
type: "tool_result",
|
|
23782
|
+
toolUseId: String(payload.call_id ?? ""),
|
|
23783
|
+
content: resultText(payload.output),
|
|
23784
|
+
isError: false
|
|
23785
|
+
});
|
|
23924
23786
|
}
|
|
23925
23787
|
}
|
|
23926
|
-
|
|
23927
|
-
|
|
23928
|
-
|
|
23929
|
-
|
|
23788
|
+
const truncatedEarlier = turns.length > maxTurns;
|
|
23789
|
+
return {
|
|
23790
|
+
sessionId,
|
|
23791
|
+
cwd,
|
|
23792
|
+
turns: truncatedEarlier ? turns.slice(turns.length - maxTurns) : turns,
|
|
23793
|
+
omitted,
|
|
23794
|
+
truncatedEarlier
|
|
23795
|
+
};
|
|
23796
|
+
}
|
|
23797
|
+
function codexArchiveMeta(headText, tailText) {
|
|
23798
|
+
let sessionId;
|
|
23799
|
+
let cwd;
|
|
23800
|
+
let title = "";
|
|
23801
|
+
for (const raw of headText.split("\n")) {
|
|
23802
|
+
if (!raw.trim()) continue;
|
|
23930
23803
|
try {
|
|
23931
|
-
const
|
|
23932
|
-
|
|
23804
|
+
const obj = JSON.parse(raw);
|
|
23805
|
+
const payload = codexPayload(obj);
|
|
23806
|
+
if (!payload) continue;
|
|
23807
|
+
if (obj.type === "session_meta") {
|
|
23808
|
+
if (!sessionId && typeof payload.id === "string") sessionId = payload.id;
|
|
23809
|
+
if (!cwd && typeof payload.cwd === "string") cwd = payload.cwd;
|
|
23810
|
+
} else if (!title && obj.type === "event_msg" && payload.type === "user_message") {
|
|
23811
|
+
title = firstLine(codexMessage(payload));
|
|
23812
|
+
}
|
|
23813
|
+
} catch {
|
|
23814
|
+
}
|
|
23815
|
+
}
|
|
23816
|
+
let lastLine = "";
|
|
23817
|
+
const lines = tailText.split("\n").filter((line) => line.trim());
|
|
23818
|
+
for (let i = lines.length - 1; i >= 0 && !lastLine; i--) {
|
|
23819
|
+
try {
|
|
23820
|
+
const obj = JSON.parse(lines[i]);
|
|
23821
|
+
const payload = codexPayload(obj);
|
|
23822
|
+
if (payload && obj.type === "event_msg" && (payload.type === "agent_message" || payload.type === "user_message")) {
|
|
23823
|
+
lastLine = firstLine(codexMessage(payload));
|
|
23824
|
+
}
|
|
23825
|
+
} catch {
|
|
23826
|
+
}
|
|
23827
|
+
}
|
|
23828
|
+
return {
|
|
23829
|
+
...sessionId ? { sessionId } : {},
|
|
23830
|
+
...cwd ? { cwd } : {},
|
|
23831
|
+
title,
|
|
23832
|
+
lastLine
|
|
23833
|
+
};
|
|
23834
|
+
}
|
|
23835
|
+
|
|
23836
|
+
// src/sessions.ts
|
|
23837
|
+
var LIST_LIMIT = 100;
|
|
23838
|
+
var CANDIDATE_LIMIT = 300;
|
|
23839
|
+
var SLICE_BYTES = 64 * 1024;
|
|
23840
|
+
var HISTORY_TURNS = 100;
|
|
23841
|
+
function archiveRoot(agent) {
|
|
23842
|
+
if (agent === "claude") {
|
|
23843
|
+
return join5(process.env.AGENTLINK_CLAUDE_HOME || join5(homedir5(), ".claude"), "projects");
|
|
23844
|
+
}
|
|
23845
|
+
return join5(
|
|
23846
|
+
process.env.AGENTLINK_CODEX_HOME || process.env.CODEX_HOME || join5(homedir5(), ".codex"),
|
|
23847
|
+
"sessions"
|
|
23848
|
+
);
|
|
23849
|
+
}
|
|
23850
|
+
function validSessionId(id) {
|
|
23851
|
+
return /^[A-Za-z0-9._-]{1,128}$/.test(id);
|
|
23852
|
+
}
|
|
23853
|
+
function archiveFiles(root, maxDepth) {
|
|
23854
|
+
const files = [];
|
|
23855
|
+
const stack = [{ dir: root, depth: 0 }];
|
|
23856
|
+
while (stack.length > 0) {
|
|
23857
|
+
const next = stack.pop();
|
|
23858
|
+
let entries;
|
|
23859
|
+
try {
|
|
23860
|
+
entries = readdirSync3(next.dir, { withFileTypes: true });
|
|
23933
23861
|
} catch {
|
|
23934
23862
|
continue;
|
|
23935
23863
|
}
|
|
23936
|
-
|
|
23937
|
-
|
|
23938
|
-
|
|
23939
|
-
|
|
23940
|
-
|
|
23941
|
-
|
|
23942
|
-
|
|
23943
|
-
|
|
23864
|
+
for (const entry of entries) {
|
|
23865
|
+
const path = join5(next.dir, entry.name);
|
|
23866
|
+
if (entry.isDirectory() && next.depth < maxDepth) {
|
|
23867
|
+
stack.push({ dir: path, depth: next.depth + 1 });
|
|
23868
|
+
} else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
|
|
23869
|
+
try {
|
|
23870
|
+
files.push({ path, mtime: statSync(path).mtimeMs });
|
|
23871
|
+
} catch {
|
|
23872
|
+
}
|
|
23873
|
+
}
|
|
23874
|
+
}
|
|
23944
23875
|
}
|
|
23945
|
-
return
|
|
23876
|
+
return files.sort((a, b) => b.mtime - a.mtime).slice(0, CANDIDATE_LIMIT);
|
|
23946
23877
|
}
|
|
23947
|
-
function
|
|
23948
|
-
const
|
|
23949
|
-
const
|
|
23950
|
-
let projectDirs;
|
|
23878
|
+
function readHeadTail(path) {
|
|
23879
|
+
const size = statSync(path).size;
|
|
23880
|
+
const fd = openSync(path, "r");
|
|
23951
23881
|
try {
|
|
23952
|
-
|
|
23953
|
-
|
|
23954
|
-
|
|
23882
|
+
const headSize = Math.min(size, SLICE_BYTES);
|
|
23883
|
+
const tailSize = Math.min(size, SLICE_BYTES);
|
|
23884
|
+
const head = Buffer.alloc(headSize);
|
|
23885
|
+
const tail = Buffer.alloc(tailSize);
|
|
23886
|
+
readSync(fd, head, 0, headSize, 0);
|
|
23887
|
+
readSync(fd, tail, 0, tailSize, Math.max(0, size - tailSize));
|
|
23888
|
+
return {
|
|
23889
|
+
head: head.toString("utf8"),
|
|
23890
|
+
tail: tail.toString("utf8")
|
|
23891
|
+
};
|
|
23892
|
+
} finally {
|
|
23893
|
+
closeSync(fd);
|
|
23955
23894
|
}
|
|
23956
|
-
|
|
23957
|
-
|
|
23895
|
+
}
|
|
23896
|
+
function scanAgent(agent) {
|
|
23897
|
+
const root = archiveRoot(agent);
|
|
23898
|
+
const depth = agent === "claude" ? 2 : 4;
|
|
23899
|
+
const sessions = [];
|
|
23900
|
+
for (const file2 of archiveFiles(root, depth)) {
|
|
23958
23901
|
try {
|
|
23959
|
-
|
|
23902
|
+
const { head, tail } = readHeadTail(file2.path);
|
|
23903
|
+
const meta3 = agent === "claude" ? claudeArchiveMeta(head, tail) : codexArchiveMeta(head, tail);
|
|
23904
|
+
const filenameId = basename(file2.path, ".jsonl");
|
|
23905
|
+
const sessionId = meta3.sessionId || (agent === "codex" ? filenameId.match(/([A-Za-z0-9]+(?:-[A-Za-z0-9]+){4})$/)?.[1] : filenameId);
|
|
23906
|
+
if (!sessionId || !validSessionId(sessionId)) continue;
|
|
23907
|
+
sessions.push({
|
|
23908
|
+
agent,
|
|
23909
|
+
sessionId,
|
|
23910
|
+
...meta3.cwd ? { cwd: meta3.cwd } : {},
|
|
23911
|
+
title: meta3.title || "(\u65E0\u6807\u9898\u4F1A\u8BDD)",
|
|
23912
|
+
lastLine: meta3.lastLine,
|
|
23913
|
+
mtime: file2.mtime
|
|
23914
|
+
});
|
|
23960
23915
|
} catch {
|
|
23961
23916
|
}
|
|
23962
23917
|
}
|
|
23963
|
-
return
|
|
23918
|
+
return sessions;
|
|
23964
23919
|
}
|
|
23965
|
-
function
|
|
23966
|
-
|
|
23967
|
-
|
|
23968
|
-
|
|
23969
|
-
|
|
23970
|
-
|
|
23971
|
-
|
|
23972
|
-
|
|
23973
|
-
|
|
23974
|
-
|
|
23975
|
-
|
|
23976
|
-
const
|
|
23977
|
-
|
|
23978
|
-
|
|
23979
|
-
|
|
23920
|
+
function scanClaudeSessions() {
|
|
23921
|
+
return scanAgent("claude").slice(0, LIST_LIMIT);
|
|
23922
|
+
}
|
|
23923
|
+
function scanCodexSessions() {
|
|
23924
|
+
return scanAgent("codex").slice(0, LIST_LIMIT);
|
|
23925
|
+
}
|
|
23926
|
+
function scanNativeSessions() {
|
|
23927
|
+
return [...scanClaudeSessions(), ...scanCodexSessions()].sort((a, b) => b.mtime - a.mtime).slice(0, LIST_LIMIT);
|
|
23928
|
+
}
|
|
23929
|
+
function archivePath(agent, sessionId) {
|
|
23930
|
+
if (!validSessionId(sessionId)) return null;
|
|
23931
|
+
const suffix = agent === "claude" ? `${sessionId}.jsonl` : `-${sessionId}.jsonl`;
|
|
23932
|
+
return archiveFiles(archiveRoot(agent), agent === "claude" ? 2 : 4).find((file2) => file2.path.endsWith(suffix))?.path ?? null;
|
|
23933
|
+
}
|
|
23934
|
+
function runsFromParsed(agent, sessionId, parsed) {
|
|
23935
|
+
return parsed.turns.map((turn, index) => ({
|
|
23936
|
+
runId: `native-${agent}-${sessionId}-${index}`,
|
|
23937
|
+
agent,
|
|
23980
23938
|
prompt: turn.prompt,
|
|
23981
23939
|
...parsed.cwd ? { cwd: parsed.cwd } : {},
|
|
23982
23940
|
sessionId,
|
|
23983
23941
|
startedAt: turn.startedAt ?? 0,
|
|
23984
23942
|
status: "done",
|
|
23943
|
+
code: 0,
|
|
23985
23944
|
truncated: false,
|
|
23986
23945
|
events: turn.events
|
|
23987
23946
|
}));
|
|
23988
|
-
|
|
23947
|
+
}
|
|
23948
|
+
function nativeSessionHistory(agent, sessionId) {
|
|
23949
|
+
if (agent !== "claude" && agent !== "codex") {
|
|
23950
|
+
return { error: `session takeover does not support "${agent}"` };
|
|
23951
|
+
}
|
|
23952
|
+
const path = archivePath(agent, sessionId);
|
|
23953
|
+
if (!path) return { error: `${agent} session "${sessionId}" was not found` };
|
|
23954
|
+
try {
|
|
23955
|
+
const text = readFileSync4(path, "utf8");
|
|
23956
|
+
const parsed = agent === "claude" ? parseClaudeArchive(text, { maxTurns: HISTORY_TURNS }) : parseCodexArchive(text, { maxTurns: HISTORY_TURNS });
|
|
23957
|
+
return {
|
|
23958
|
+
runs: runsFromParsed(agent, sessionId, parsed),
|
|
23959
|
+
truncatedEarlier: parsed.truncatedEarlier
|
|
23960
|
+
};
|
|
23961
|
+
} catch (err) {
|
|
23962
|
+
return {
|
|
23963
|
+
error: `cannot read ${agent} session "${sessionId}": ${err instanceof Error ? err.message : String(err)}`
|
|
23964
|
+
};
|
|
23965
|
+
}
|
|
23989
23966
|
}
|
|
23990
23967
|
|
|
23991
23968
|
// src/tunnel.ts
|
|
@@ -24365,26 +24342,43 @@ async function probeVersion(bin) {
|
|
|
24365
24342
|
return first || void 0;
|
|
24366
24343
|
}
|
|
24367
24344
|
async function probeAgents() {
|
|
24368
|
-
|
|
24345
|
+
const agents2 = await Promise.all(
|
|
24369
24346
|
REGISTRY.map(async (def) => {
|
|
24370
|
-
const
|
|
24371
|
-
if (!
|
|
24347
|
+
const adapter = getAgent(def.id);
|
|
24348
|
+
if (!adapter) return { id: def.id, label: def.label, detected: false, models: def.models };
|
|
24372
24349
|
const [version2, probed] = await Promise.all([
|
|
24373
|
-
probeVersion(bin),
|
|
24374
|
-
def.probeModels ? def.probeModels(bin) : Promise.resolve([])
|
|
24350
|
+
probeVersion(resolveBin(def.command ?? def.acp.bin) ?? adapter.bin),
|
|
24351
|
+
def.probeModels ? def.probeModels(adapter.bin) : Promise.resolve([])
|
|
24375
24352
|
]);
|
|
24376
24353
|
const models = [.../* @__PURE__ */ new Set([...probed, ...def.models])];
|
|
24377
24354
|
return {
|
|
24378
24355
|
id: def.id,
|
|
24379
24356
|
label: def.label,
|
|
24380
24357
|
detected: true,
|
|
24381
|
-
bin,
|
|
24358
|
+
bin: adapter.bin,
|
|
24382
24359
|
models,
|
|
24383
24360
|
...version2 ? { version: version2 } : {},
|
|
24384
24361
|
...APPROVAL ? { approval: true } : {}
|
|
24385
24362
|
};
|
|
24386
24363
|
})
|
|
24387
24364
|
);
|
|
24365
|
+
let joycodeBin = resolveBin("joycode");
|
|
24366
|
+
if (!joycodeBin) {
|
|
24367
|
+
try {
|
|
24368
|
+
joycodeBin = realpathSync("/Applications/JoyCode.app/Contents/Resources/app/bin/joycode");
|
|
24369
|
+
} catch {
|
|
24370
|
+
}
|
|
24371
|
+
}
|
|
24372
|
+
if (joycodeBin) {
|
|
24373
|
+
agents2.push({
|
|
24374
|
+
id: "joycode",
|
|
24375
|
+
label: "JoyCode\uFF08\u5DF2\u5B89\u88C5\uFF0C\u6682\u65E0\u63A5\u7BA1\u63A5\u53E3\uFF09",
|
|
24376
|
+
detected: false,
|
|
24377
|
+
bin: joycodeBin,
|
|
24378
|
+
models: []
|
|
24379
|
+
});
|
|
24380
|
+
}
|
|
24381
|
+
return agents2;
|
|
24388
24382
|
}
|
|
24389
24383
|
var running = /* @__PURE__ */ new Map();
|
|
24390
24384
|
var approvalRuns = /* @__PURE__ */ new Map();
|
|
@@ -24415,7 +24409,7 @@ function handleRun(ws, { requestId, agent: agentId, prompt: rawPrompt, sessionId
|
|
|
24415
24409
|
};
|
|
24416
24410
|
const adapter = getAgent(agentId);
|
|
24417
24411
|
if (!adapter) return fail(`agent "${agentId}" not found`);
|
|
24418
|
-
const { plain: prompt
|
|
24412
|
+
const { plain: prompt } = decodePrompt(rawPrompt);
|
|
24419
24413
|
if (typeof prompt !== "string") {
|
|
24420
24414
|
return fail(ENCKEY ? `malformed run message: prompt could not be decrypted (wrong key?)` : `malformed run message: prompt is not a string`);
|
|
24421
24415
|
}
|
|
@@ -24425,13 +24419,6 @@ function handleRun(ws, { requestId, agent: agentId, prompt: rawPrompt, sessionId
|
|
|
24425
24419
|
if (!real) return fail(`cwd rejected: ${error51}`);
|
|
24426
24420
|
runCwd = real;
|
|
24427
24421
|
}
|
|
24428
|
-
const recorder = recordRun(conversationId, {
|
|
24429
|
-
runId: requestId,
|
|
24430
|
-
agent: agentId,
|
|
24431
|
-
prompt: storedPrompt,
|
|
24432
|
-
...cwd !== void 0 ? { cwd } : {},
|
|
24433
|
-
...typeof model === "string" && model ? { model } : {}
|
|
24434
|
-
});
|
|
24435
24422
|
runAcpPath(ws, {
|
|
24436
24423
|
requestId,
|
|
24437
24424
|
adapter,
|
|
@@ -24440,8 +24427,7 @@ function handleRun(ws, { requestId, agent: agentId, prompt: rawPrompt, sessionId
|
|
|
24440
24427
|
runCwd,
|
|
24441
24428
|
model: typeof model === "string" && model ? model : void 0,
|
|
24442
24429
|
conversationId,
|
|
24443
|
-
notifyDetail
|
|
24444
|
-
recorder
|
|
24430
|
+
notifyDetail
|
|
24445
24431
|
});
|
|
24446
24432
|
}
|
|
24447
24433
|
var NOTIFY_TITLE_MAX = 200;
|
|
@@ -24455,7 +24441,7 @@ function approvalNotify(tool, input) {
|
|
|
24455
24441
|
return { title: `\u5BA1\u6279\uFF1A${tool}`.slice(0, NOTIFY_TITLE_MAX), summary: firstLine2.slice(0, NOTIFY_SUMMARY_MAX) };
|
|
24456
24442
|
}
|
|
24457
24443
|
function runAcpPath(ws, opts) {
|
|
24458
|
-
const { requestId, adapter
|
|
24444
|
+
const { requestId, adapter } = opts;
|
|
24459
24445
|
console.log(`[daemon] requestId=${requestId} starting ${adapter.id} via ACP (approval=${APPROVAL}, cwd=${opts.runCwd})`);
|
|
24460
24446
|
let lastText;
|
|
24461
24447
|
const emitEvent = (evt) => {
|
|
@@ -24463,10 +24449,8 @@ function runAcpPath(ws, opts) {
|
|
|
24463
24449
|
if (ENCKEY) {
|
|
24464
24450
|
const capped = truncateEventContent(evt);
|
|
24465
24451
|
const envelope = encryptEvent(ENCKEY, capped);
|
|
24466
|
-
recorder?.event(capped, envelope);
|
|
24467
24452
|
send(ws, { type: "event", requestId, event: envelope });
|
|
24468
24453
|
} else {
|
|
24469
|
-
recorder?.event(evt);
|
|
24470
24454
|
send(ws, { type: "event", requestId, event: evt });
|
|
24471
24455
|
}
|
|
24472
24456
|
};
|
|
@@ -24487,10 +24471,8 @@ function runAcpPath(ws, opts) {
|
|
|
24487
24471
|
if (ENCKEY) {
|
|
24488
24472
|
const capped = truncateEventContent(evt);
|
|
24489
24473
|
wireEvent = encryptEvent(ENCKEY, capped);
|
|
24490
|
-
if (!renotify) recorder?.event(capped, wireEvent);
|
|
24491
24474
|
} else {
|
|
24492
24475
|
wireEvent = evt;
|
|
24493
|
-
if (!renotify) recorder?.event(evt);
|
|
24494
24476
|
}
|
|
24495
24477
|
send(ws, {
|
|
24496
24478
|
type: "permission_request",
|
|
@@ -24518,9 +24500,6 @@ function runAcpPath(ws, opts) {
|
|
|
24518
24500
|
acpArgs: adapter.acp.args,
|
|
24519
24501
|
env: spawnEnv(),
|
|
24520
24502
|
onEvent: emitEvent,
|
|
24521
|
-
onTitle: (title) => {
|
|
24522
|
-
if (opts.conversationId) putConversation({ id: opts.conversationId, title });
|
|
24523
|
-
},
|
|
24524
24503
|
requestPermission,
|
|
24525
24504
|
signal: controller.signal,
|
|
24526
24505
|
onSpawn: (child) => void running.set(requestId, child)
|
|
@@ -24542,7 +24521,6 @@ function runAcpPath(ws, opts) {
|
|
|
24542
24521
|
if (error51) done.error = error51;
|
|
24543
24522
|
if (opts.notifyDetail === true) done.notify = { title: adapter.id, summary: notifySummary(lastText) };
|
|
24544
24523
|
console.log(`[daemon] requestId=${requestId} closed code=${code}${error51 ? ` error=${error51}` : ""} (ACP)`);
|
|
24545
|
-
recorder?.done(code, error51);
|
|
24546
24524
|
send(ws, done);
|
|
24547
24525
|
});
|
|
24548
24526
|
}
|
|
@@ -24628,30 +24606,28 @@ function handleListdir(ws, { requestId, path }) {
|
|
|
24628
24606
|
}
|
|
24629
24607
|
}
|
|
24630
24608
|
function handleConvList(ws, requestId) {
|
|
24631
|
-
send(ws, { type: "conv_list_result", requestId, conversations:
|
|
24609
|
+
send(ws, { type: "conv_list_result", requestId, conversations: [] });
|
|
24632
24610
|
}
|
|
24633
|
-
function handleConvPut(ws, requestId,
|
|
24634
|
-
|
|
24635
|
-
|
|
24636
|
-
|
|
24637
|
-
|
|
24638
|
-
|
|
24611
|
+
function handleConvPut(ws, requestId, _conversation) {
|
|
24612
|
+
send(ws, {
|
|
24613
|
+
type: "conv_put_result",
|
|
24614
|
+
requestId,
|
|
24615
|
+
error: "AgentLink conversation storage is disabled; use native sessions"
|
|
24616
|
+
});
|
|
24639
24617
|
}
|
|
24640
|
-
function handleConvDelete(ws, requestId,
|
|
24641
|
-
if (typeof id === "string") deleteConversation(id);
|
|
24618
|
+
function handleConvDelete(ws, requestId, _id) {
|
|
24642
24619
|
send(ws, { type: "conv_delete_result", requestId });
|
|
24643
24620
|
}
|
|
24644
24621
|
function handleConvClear(ws, requestId) {
|
|
24645
|
-
clearConversations();
|
|
24646
24622
|
send(ws, { type: "conv_clear_result", requestId });
|
|
24647
24623
|
}
|
|
24648
|
-
function handleHistory(ws, requestId,
|
|
24649
|
-
send(ws, { type: "history_result", requestId, runs:
|
|
24624
|
+
function handleHistory(ws, requestId, _conversationId) {
|
|
24625
|
+
send(ws, { type: "history_result", requestId, runs: [] });
|
|
24650
24626
|
}
|
|
24651
24627
|
function handleScanSessions(ws, requestId) {
|
|
24652
24628
|
let sessions;
|
|
24653
24629
|
try {
|
|
24654
|
-
const list =
|
|
24630
|
+
const list = scanNativeSessions();
|
|
24655
24631
|
sessions = ENCKEY ? encryptEvent(ENCKEY, list) : list;
|
|
24656
24632
|
} catch (err) {
|
|
24657
24633
|
return send(ws, { type: "scan_sessions_result", requestId, sessions: ENCKEY ? void 0 : [], error: err instanceof Error ? err.message : String(err) });
|
|
@@ -24662,7 +24638,7 @@ function handleSessionHistory(ws, requestId, agent, sessionId) {
|
|
|
24662
24638
|
if (typeof agent !== "string" || typeof sessionId !== "string") {
|
|
24663
24639
|
return send(ws, { type: "session_history_result", requestId, runs: [], error: "malformed session_history: agent and sessionId are required" });
|
|
24664
24640
|
}
|
|
24665
|
-
const result =
|
|
24641
|
+
const result = nativeSessionHistory(agent, sessionId);
|
|
24666
24642
|
if ("error" in result) {
|
|
24667
24643
|
return send(ws, { type: "session_history_result", requestId, runs: [], error: result.error });
|
|
24668
24644
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "alink-cli",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.2",
|
|
4
4
|
"description": "一条命令把工作机接入 AgentLink,随时随地遥控本机的编码 agent。One command to link your machine to AgentLink and control your coding agents from anywhere.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -53,6 +53,8 @@
|
|
|
53
53
|
"ws": "^8.18.0"
|
|
54
54
|
},
|
|
55
55
|
"dependencies": {
|
|
56
|
-
"@agentclientprotocol/
|
|
56
|
+
"@agentclientprotocol/codex-acp": "1.1.9",
|
|
57
|
+
"@agentclientprotocol/sdk": "^1.3.0",
|
|
58
|
+
"@zed-industries/claude-code-acp": "0.16.2"
|
|
57
59
|
}
|
|
58
60
|
}
|