lark-remote 0.1.1 → 0.1.3

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 (50) hide show
  1. package/README.en.md +2 -5
  2. package/README.md +1 -4
  3. package/dist/bridge/index.d.ts +1 -3
  4. package/dist/bridge/index.js +11 -12
  5. package/dist/card/card-budget.js +1 -2
  6. package/dist/card/card-shared.d.ts +8 -0
  7. package/dist/card/card-shared.js +14 -0
  8. package/dist/card/run-renderer.js +1 -0
  9. package/dist/card/run-state.d.ts +4 -0
  10. package/dist/config/codex-config.d.ts +3 -5
  11. package/dist/config/codex-config.js +7 -7
  12. package/dist/config/dir.d.ts +1 -0
  13. package/dist/config/dir.js +4 -0
  14. package/dist/config/index.d.ts +0 -10
  15. package/dist/config/index.js +0 -11
  16. package/dist/connector/index.js +28 -0
  17. package/dist/index.js +89 -41
  18. package/dist/router/config/codex.js +8 -12
  19. package/dist/router/index.d.ts +15 -2
  20. package/dist/router/index.js +314 -44
  21. package/dist/router/utils.d.ts +3 -0
  22. package/dist/router/utils.js +15 -3
  23. package/dist/runner/claude/runner.d.ts +0 -1
  24. package/dist/runner/claude/runner.js +1 -1
  25. package/dist/runner/codex/runner.d.ts +0 -2
  26. package/dist/runner/codex/runner.js +1 -1
  27. package/dist/runner/common/spawning-runner.d.ts +0 -1
  28. package/dist/runner/common/spawning-runner.js +2 -4
  29. package/dist/runner/kimi/runner.d.ts +0 -1
  30. package/dist/runner/kimi/runner.js +1 -1
  31. package/dist/runner/opencode/runner.d.ts +0 -2
  32. package/dist/runner/opencode/runner.js +1 -1
  33. package/dist/runner/pi/runner.d.ts +0 -1
  34. package/dist/runner/pi/runner.js +1 -1
  35. package/dist/runner/probe.d.ts +45 -0
  36. package/dist/runner/probe.js +87 -0
  37. package/dist/runner/types.d.ts +3 -0
  38. package/dist/session/claude/sessions.d.ts +8 -6
  39. package/dist/session/claude/sessions.js +178 -26
  40. package/dist/session/codex/rollout-reader.d.ts +1 -0
  41. package/dist/session/codex/rollout-reader.js +15 -0
  42. package/dist/session/codex/sessions.d.ts +1 -1
  43. package/dist/session/codex/sessions.js +2 -1
  44. package/dist/session/kimi/sessions.d.ts +3 -1
  45. package/dist/session/kimi/sessions.js +42 -6
  46. package/dist/session/opencode/sessions.d.ts +0 -2
  47. package/dist/session/opencode/sessions.js +1 -1
  48. package/dist/startup-contact.d.ts +4 -2
  49. package/dist/startup-contact.js +7 -4
  50. package/package.json +39 -4
@@ -27,10 +27,11 @@ export class CodexSessionReader {
27
27
  getNewestSession(cwd) {
28
28
  return this.listSessions(cwd, { limit: 1 }).sessions[0] ?? null;
29
29
  }
30
- readSessionContent(sessionId, _cwd, opts) {
30
+ readSessionContent(sessionId, cwd, opts) {
31
31
  return readCodexSessionContent(sessionId, {
32
32
  codexHome: this.codexHome,
33
33
  maxEvents: opts?.maxEvents,
34
+ cwd,
34
35
  });
35
36
  }
36
37
  isSessionActive(sessionId, _cwd) {
@@ -24,7 +24,9 @@ export type CwdGuardResult = 'verified' | 'failed' | 'unverifiable';
24
24
  * - failed: session has a workDir but it doesn't match realCwd
25
25
  * - unverifiable: session has no workDir field (cannot verify)
26
26
  *
27
- * Unverifiable is a fail-open state — the caller should allow access and log a warning.
27
+ * Unverifiable sessions are handled by the caller: when no cwd source can
28
+ * verify the session, it is rejected (fail-closed), aligned with claude's
29
+ * cwd guard semantics.
28
30
  */
29
31
  export declare function checkCwdGuard(state: KimiSessionState, realCwd: string): CwdGuardResult;
30
32
  /**
@@ -105,7 +105,9 @@ export function extractWorkDir(state) {
105
105
  * - failed: session has a workDir but it doesn't match realCwd
106
106
  * - unverifiable: session has no workDir field (cannot verify)
107
107
  *
108
- * Unverifiable is a fail-open state — the caller should allow access and log a warning.
108
+ * Unverifiable sessions are handled by the caller: when no cwd source can
109
+ * verify the session, it is rejected (fail-closed), aligned with claude's
110
+ * cwd guard semantics.
109
111
  */
110
112
  export function checkCwdGuard(state, realCwd) {
111
113
  const sessionWorkDir = extractWorkDir(state);
@@ -283,27 +285,61 @@ export class KimiSessionReader {
283
285
  * Read session content (events + usage)
284
286
  */
285
287
  readSessionContent(sessionId, cwd, opts) {
286
- // Find session directory from index
287
- const sessionDir = this.findSessionDirFromIndex(sessionId);
288
+ // Find session directory from index (also retrieve index entry for
289
+ // fallback cwd guard when state.json is missing or unparseable)
290
+ const indexEntry = this.findSessionIndexEntry(sessionId);
291
+ const sessionDir = indexEntry?.sessionDir ?? null;
288
292
  if (!sessionDir) {
289
293
  return { events: [] };
290
294
  }
291
295
  const statePath = path.join(sessionDir, 'state.json');
296
+ let realCwd;
297
+ try {
298
+ realCwd = fs.realpathSync(cwd);
299
+ }
300
+ catch {
301
+ getLogger().warn(`[kimi-session-reader] fs.realpathSync failed for cwd=${cwd} (session ${sessionId}), returning empty`);
302
+ return { events: [] };
303
+ }
304
+ let cwdGuardPassed = false;
292
305
  try {
293
306
  if (fs.existsSync(statePath)) {
294
307
  const state = JSON.parse(fs.readFileSync(statePath, 'utf-8'));
295
- const realCwd = fs.realpathSync(cwd);
296
308
  const guardResult = checkCwdGuard(state, realCwd);
297
309
  if (guardResult === 'failed') {
298
310
  return { events: [] };
299
311
  }
300
312
  if (guardResult === 'unverifiable') {
301
- getLogger().warn(`[kimi-session-reader] state.json has no workDir/cwd field for session ${sessionId}, skipping cwd guard`);
313
+ getLogger().warn(`[kimi-session-reader] state.json has no workDir/cwd field for session ${sessionId}, falling back to index workDir`);
314
+ // Fall through to index-based guard below
315
+ }
316
+ else {
317
+ // verified — state.json cwd matches
318
+ cwdGuardPassed = true;
302
319
  }
303
320
  }
304
321
  }
305
322
  catch {
306
- // Unverifiable — skip the check
323
+ // state.json unparseable — fall through to index-based guard
324
+ getLogger().warn(`[kimi-session-reader] state.json parse failed for session ${sessionId}, falling back to index workDir`);
325
+ }
326
+ // Fallback: when state.json is missing, unparseable, or has no workDir/cwd,
327
+ // use the workDir from the session index entry as a secondary guard.
328
+ // This prevents /resume <id> from accessing a session belonging to a
329
+ // different workspace when state.json is unavailable.
330
+ if (!cwdGuardPassed) {
331
+ const indexWorkDir = indexEntry?.workDir;
332
+ if (indexWorkDir && indexWorkDir !== realCwd) {
333
+ return { events: [] };
334
+ }
335
+ // v2 sessions have empty index workDir (v1-only field) and may lack
336
+ // state.json cwd. When no cwd source can verify the session belongs to
337
+ // the requested workspace, fail-closed to prevent cross-workspace access
338
+ // (aligned with claude's fail-closed cwd guard).
339
+ if (!indexWorkDir) {
340
+ getLogger().warn(`[kimi-session-reader] no cwd source (state.json + index) for session ${sessionId}, rejecting (fail-closed)`);
341
+ return { events: [] };
342
+ }
307
343
  }
308
344
  const wirePath = path.join(sessionDir, 'agents', 'main', 'wire.jsonl');
309
345
  if (!fs.existsSync(wirePath)) {
@@ -15,8 +15,6 @@
15
15
  */
16
16
  import type { AgentSession, AgentSessionReader, SessionContent } from '../../runner/index.js';
17
17
  interface OpencodeSessionReaderOptions {
18
- /** Path to opencode binary. Default: 'opencode' (in PATH). */
19
- binary?: string;
20
18
  /** Cache TTL for session list (ms). Default: 10_000. */
21
19
  cacheTtlMs?: number;
22
20
  /**
@@ -28,7 +28,7 @@ export class OpencodeSessionReader {
28
28
  /** Cache keyed by cwd - each cwd has its own cache entry */
29
29
  listCache = new Map();
30
30
  constructor(opts = {}) {
31
- this.binary = opts.binary ?? 'opencode';
31
+ this.binary = 'opencode';
32
32
  this.cacheTtlMs = opts.cacheTtlMs ?? 10_000;
33
33
  this.captureExportOverride = opts.captureExport;
34
34
  }
@@ -13,6 +13,8 @@ export declare class StartupContactStore {
13
13
  save(contact: StartupContact): void;
14
14
  getContact(): StartupContact | undefined;
15
15
  }
16
- export declare function formatStartupHello(now?: Date, pid?: number): string;
17
- export declare function sendStartupHello(connector: StartupHelloChannel, store: StartupContactStore): Promise<void>;
16
+ export declare function formatStartupHello(now?: Date, pid?: number, dev?: boolean): string;
17
+ export declare function sendStartupHello(connector: StartupHelloChannel, store: StartupContactStore, opts?: {
18
+ dev?: boolean;
19
+ }): Promise<void>;
18
20
  export {};
@@ -21,7 +21,7 @@ export class StartupContactStore {
21
21
  return contact.chatId || contact.userId ? contact : undefined;
22
22
  }
23
23
  }
24
- export function formatStartupHello(now = new Date(), pid = process.pid) {
24
+ export function formatStartupHello(now = new Date(), pid = process.pid, dev = false) {
25
25
  const startedAt = new Intl.DateTimeFormat('zh-CN', {
26
26
  year: 'numeric',
27
27
  month: '2-digit',
@@ -31,9 +31,10 @@ export function formatStartupHello(now = new Date(), pid = process.pid) {
31
31
  second: '2-digit',
32
32
  hour12: false,
33
33
  }).format(now);
34
- return `lark-remote 已启动\n启动时间:${startedAt}\n进程号:${pid}`;
34
+ const modeTag = dev ? ' 🔧 dev' : '';
35
+ return `lark-remote${modeTag} 已启动\n启动时间:${startedAt}\n进程号:${pid}`;
35
36
  }
36
- export async function sendStartupHello(connector, store) {
37
+ export async function sendStartupHello(connector, store, opts) {
37
38
  const contact = store.getContact();
38
39
  const recipient = contact?.chatId ?? contact?.userId;
39
40
  if (!recipient) {
@@ -41,7 +42,9 @@ export async function sendStartupHello(connector, store) {
41
42
  return;
42
43
  }
43
44
  try {
44
- await connector.sendWithRetry(recipient, { text: formatStartupHello() });
45
+ await connector.sendWithRetry(recipient, {
46
+ text: formatStartupHello(undefined, undefined, opts?.dev),
47
+ });
45
48
  getLogger().info('[startup] hello message sent');
46
49
  }
47
50
  catch (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lark-remote",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Feishu (Lark) private-chat bridge to local coding agents — Claude Code / Codex / opencode / pi / kimi, with live-streaming CardKit 2.0 cards",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -18,13 +18,13 @@
18
18
  "!dist/**/*.js.map",
19
19
  "!dist/**/*.d.ts.map",
20
20
  "!dist/config/codex-bundled-test-helpers.*",
21
- "!dist/test-helpers.*",
22
21
  "README.md",
23
22
  "README.en.md",
24
23
  "LICENSE",
25
24
  "CHANGELOG.md"
26
25
  ],
27
26
  "scripts": {
27
+ "prepare": "husky",
28
28
  "dev": "bun src/index.ts",
29
29
  "build": "rm -rf dist && tsc && chmod +x dist/index.js dist/cli.js",
30
30
  "typecheck": "tsc --noEmit",
@@ -34,12 +34,35 @@
34
34
  "format:check": "prettier --check 'src/**/*.ts' 'tests/**/*.ts'",
35
35
  "test": "vitest run",
36
36
  "test:watch": "vitest",
37
+ "test:bridge": "vitest run --project infra-bridge",
38
+ "test:card": "vitest run --project infra-card",
39
+ "test:connector": "vitest run --project infra-connector",
40
+ "test:router": "vitest run --project infra-router",
41
+ "test:session": "vitest run --project infra-session",
42
+ "test:runner": "vitest run --project infra-runner",
43
+ "test:config": "vitest run --project infra-config",
44
+ "test:order": "vitest run --project infra-order",
45
+ "test:misc": "vitest run --project infra-misc",
46
+ "test:claude": "vitest run --project agent-claude",
47
+ "test:codex": "vitest run --project agent-codex",
48
+ "test:opencode": "vitest run --project agent-opencode",
49
+ "test:pi": "vitest run --project agent-pi",
50
+ "test:kimi": "vitest run --project agent-kimi",
51
+ "test:bash": "vitest run --project agent-bash",
52
+ "test:infra": "vitest run --project 'infra-*'",
53
+ "test:agents": "vitest run --project 'agent-*'",
54
+ "test:codex-full": "vitest run --project agent-codex --project infra-runner --project infra-session",
55
+ "test:claude-full": "vitest run --project agent-claude --project infra-runner --project infra-session",
56
+ "test:opencode-full": "vitest run --project agent-opencode --project infra-runner --project infra-session",
57
+ "test:pi-full": "vitest run --project agent-pi --project infra-runner --project infra-session",
58
+ "test:kimi-full": "vitest run --project agent-kimi --project infra-runner --project infra-session",
59
+ "test:live": "vitest run --project live",
37
60
  "prepublishOnly": "tsc --noEmit && vitest run && rm -rf dist && tsc && chmod +x dist/index.js dist/cli.js",
38
61
  "prepack": "node -e \"const fs=require('fs');if(!fs.existsSync('dist/cli.js')){console.error('dist/ missing — run bun run build first');process.exit(1)}\""
39
62
  },
40
63
  "publishConfig": {
41
64
  "access": "public",
42
- "provenance": false
65
+ "provenance": true
43
66
  },
44
67
  "keywords": [
45
68
  "feishu",
@@ -85,9 +108,21 @@
85
108
  "eslint": "^10.7.0",
86
109
  "eslint-config-prettier": "^10.1.8",
87
110
  "eslint-plugin-prettier": "^5.5.6",
111
+ "husky": "^9.1.7",
112
+ "lint-staged": "^17.3.0",
88
113
  "prettier": "^3.9.5",
89
114
  "typescript": "^6.0.0",
90
115
  "typescript-eslint": "^8.63.0",
91
- "vitest": "^4.1.9"
116
+ "vitest": "^4.1.10"
117
+ },
118
+ "lint-staged": {
119
+ "src/**/*.ts": [
120
+ "eslint --fix",
121
+ "prettier --write"
122
+ ],
123
+ "tests/**/*.ts": [
124
+ "eslint --fix",
125
+ "prettier --write"
126
+ ]
92
127
  }
93
128
  }