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.
Files changed (2) hide show
  1. package/dist/daemon.js +273 -297
  2. 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
- return bin ? [{ ...def, bin }] : [];
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 { closeSync, openSync, readdirSync as readdirSync3, readSync, readFileSync as readFileSync4, statSync } from "node:fs";
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
- // src/sessions.ts
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 scanClaudeSessions() {
23901
- const root = claudeProjectsDir();
23902
- let projectDirs;
23903
- try {
23904
- projectDirs = readdirSync3(root, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => join5(root, e.name));
23905
- } catch {
23906
- return [];
23907
- }
23908
- const candidates = [];
23909
- for (const dir of projectDirs) {
23910
- let entries;
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
- entries = readdirSync3(dir).filter((f) => f.endsWith(".jsonl"));
23736
+ obj = JSON.parse(raw);
23913
23737
  } catch {
23738
+ omitted++;
23914
23739
  continue;
23915
23740
  }
23916
- for (const name of entries) {
23917
- const file2 = join5(dir, name);
23918
- try {
23919
- const st = statSync(file2);
23920
- if (!st.isFile() || st.size === 0) continue;
23921
- candidates.push({ file: file2, sessionId: name.replace(/\.jsonl$/, ""), mtime: st.mtimeMs, size: st.size });
23922
- } catch {
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
- candidates.sort((a, b) => b.mtime - a.mtime);
23927
- const sessions = [];
23928
- for (const c of candidates.slice(0, MAX_SESSIONS)) {
23929
- let meta3;
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 { head, tail } = readHeadTail(c.file, c.size);
23932
- meta3 = claudeArchiveMeta(head, tail);
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
- sessions.push({
23937
- agent: "claude",
23938
- sessionId: c.sessionId,
23939
- ...meta3.cwd ? { cwd: meta3.cwd } : {},
23940
- title: meta3.title || "(\u65E0\u6807\u9898\u4F1A\u8BDD)",
23941
- lastLine: meta3.lastLine,
23942
- mtime: Math.round(c.mtime)
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 sessions;
23876
+ return files.sort((a, b) => b.mtime - a.mtime).slice(0, CANDIDATE_LIMIT);
23946
23877
  }
23947
- function findClaudeArchive(sessionId) {
23948
- const root = claudeProjectsDir();
23949
- const target = `${sessionId}.jsonl`;
23950
- let projectDirs;
23878
+ function readHeadTail(path) {
23879
+ const size = statSync(path).size;
23880
+ const fd = openSync(path, "r");
23951
23881
  try {
23952
- projectDirs = readdirSync3(root, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
23953
- } catch {
23954
- return null;
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
- for (const d of projectDirs) {
23957
- const file2 = join5(root, d, target);
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
- if (statSync(file2).isFile()) return file2;
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 null;
23918
+ return sessions;
23964
23919
  }
23965
- function claudeSessionHistory(agent, sessionId) {
23966
- if (agent !== "claude") return { error: `session takeover supports claude only in M1 (got "${agent}")` };
23967
- if (!SESSION_ID_RE.test(sessionId)) return { error: "malformed sessionId" };
23968
- const file2 = findClaudeArchive(sessionId);
23969
- if (!file2) return { error: `no archive found for session "${sessionId}"` };
23970
- let text;
23971
- try {
23972
- text = readFileSync4(file2, "utf-8");
23973
- } catch (err) {
23974
- return { error: `cannot read archive: ${err instanceof Error ? err.message : String(err)}` };
23975
- }
23976
- const parsed = parseClaudeArchive(text, { maxTurns: MAX_HISTORY_TURNS });
23977
- const runs = parsed.turns.map((turn, i) => ({
23978
- runId: `${sessionId}~${i}`,
23979
- agent: "claude",
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
- return { runs, truncatedEarlier: parsed.truncatedEarlier };
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
- return Promise.all(
24345
+ const agents2 = await Promise.all(
24369
24346
  REGISTRY.map(async (def) => {
24370
- const bin = getAgent(def.id)?.bin;
24371
- if (!bin) return { id: def.id, label: def.label, detected: false, models: def.models };
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, stored: storedPrompt } = decodePrompt(rawPrompt);
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, recorder } = opts;
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: listConversations() });
24609
+ send(ws, { type: "conv_list_result", requestId, conversations: [] });
24632
24610
  }
24633
- function handleConvPut(ws, requestId, conversation) {
24634
- if (typeof conversation !== "object" || conversation === null) {
24635
- return send(ws, { type: "conv_put_result", requestId, error: "malformed conv_put: conversation is not an object" });
24636
- }
24637
- const { conversation: merged, error: error51 } = putConversation(conversation);
24638
- send(ws, { type: "conv_put_result", requestId, ...merged ? { conversation: merged } : {}, ...error51 ? { error: error51 } : {} });
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, id) {
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, conversationId) {
24649
- send(ws, { type: "history_result", requestId, runs: history(conversationId) });
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 = scanClaudeSessions();
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 = claudeSessionHistory(agent, sessionId);
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.1",
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/sdk": "^1.3.0"
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
  }