lark-coding-assistant 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +329 -0
- package/bin/lark-coding-assistant-hook.mjs +2 -0
- package/bin/lark-coding-assistant.mjs +2 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +963 -0
- package/dist/cli.js.map +1 -0
- package/dist/daemon-entry.d.ts +2 -0
- package/dist/daemon-entry.js +2794 -0
- package/dist/daemon-entry.js.map +1 -0
- package/dist/hook-entry.d.ts +2 -0
- package/dist/hook-entry.js +95 -0
- package/dist/hook-entry.js.map +1 -0
- package/package.json +55 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// src/hook-entry.ts
|
|
2
|
+
import { appendFile } from "fs/promises";
|
|
3
|
+
|
|
4
|
+
// src/agents/stop-event.ts
|
|
5
|
+
function normalizeStopEvent(bridgeSessionId, value) {
|
|
6
|
+
if (!bridgeSessionId || !value || typeof value !== "object") return void 0;
|
|
7
|
+
const event = value;
|
|
8
|
+
if (event.hook_event_name !== "Stop") return void 0;
|
|
9
|
+
const eventId = stringField(event, "turn_id") ?? stringField(event, "prompt_id");
|
|
10
|
+
const agentSessionId = stringField(event, "session_id");
|
|
11
|
+
const cwd = stringField(event, "cwd");
|
|
12
|
+
const lastAssistantMessage = stringField(event, "last_assistant_message")?.trim();
|
|
13
|
+
if (!eventId || !agentSessionId || !cwd || !lastAssistantMessage) return void 0;
|
|
14
|
+
return { sessionId: bridgeSessionId, eventId, agentSessionId, cwd, lastAssistantMessage };
|
|
15
|
+
}
|
|
16
|
+
function stringField(value, key) {
|
|
17
|
+
const field = value[key];
|
|
18
|
+
return typeof field === "string" && field.length > 0 ? field : void 0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// src/daemon/client.ts
|
|
22
|
+
import { randomUUID } from "crypto";
|
|
23
|
+
import { createConnection } from "net";
|
|
24
|
+
function requestDaemon(socketPath, request, timeoutMs = 5e3) {
|
|
25
|
+
return new Promise((resolve, reject) => {
|
|
26
|
+
const id = randomUUID();
|
|
27
|
+
const socket2 = createConnection(socketPath);
|
|
28
|
+
let buffer = "";
|
|
29
|
+
const timer = setTimeout(() => {
|
|
30
|
+
socket2.destroy();
|
|
31
|
+
reject(new Error("daemon request timed out"));
|
|
32
|
+
}, timeoutMs);
|
|
33
|
+
socket2.setEncoding("utf8");
|
|
34
|
+
socket2.once("connect", () => socket2.write(`${JSON.stringify({ ...request, id })}
|
|
35
|
+
`));
|
|
36
|
+
socket2.on("data", (chunk) => {
|
|
37
|
+
buffer += chunk;
|
|
38
|
+
const newline = buffer.indexOf("\n");
|
|
39
|
+
if (newline === -1) return;
|
|
40
|
+
clearTimeout(timer);
|
|
41
|
+
socket2.destroy();
|
|
42
|
+
try {
|
|
43
|
+
resolve(JSON.parse(buffer.slice(0, newline)));
|
|
44
|
+
} catch (error) {
|
|
45
|
+
reject(error);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
socket2.once("error", (error) => {
|
|
49
|
+
clearTimeout(timer);
|
|
50
|
+
reject(error);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// src/core/paths.ts
|
|
56
|
+
import { homedir } from "os";
|
|
57
|
+
import { join } from "path";
|
|
58
|
+
function resolveAppPaths(root = process.env.LARK_CODING_ASSISTANT_HOME) {
|
|
59
|
+
const base = root || join(homedir(), ".lark-coding-assistant");
|
|
60
|
+
return {
|
|
61
|
+
root: base,
|
|
62
|
+
config: join(base, "config.json"),
|
|
63
|
+
secrets: join(base, "secrets.json"),
|
|
64
|
+
state: join(base, "state.json"),
|
|
65
|
+
logsDir: join(base, "logs"),
|
|
66
|
+
logFile: join(base, "logs", "assistant.log"),
|
|
67
|
+
runtimeDir: join(base, "runtime"),
|
|
68
|
+
socket: join(base, "runtime", "daemon.sock"),
|
|
69
|
+
pid: join(base, "runtime", "daemon.pid")
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// src/hook-entry.ts
|
|
74
|
+
var socket = process.env.LARK_CODING_ASSISTANT_SOCKET;
|
|
75
|
+
var sessionId = process.env.LARK_CODING_ASSISTANT_SESSION_ID;
|
|
76
|
+
try {
|
|
77
|
+
if (!socket || !sessionId) throw new Error("missing bridge hook environment");
|
|
78
|
+
const payload = JSON.parse(await readStdin());
|
|
79
|
+
const candidate = normalizeStopEvent(sessionId, payload);
|
|
80
|
+
if (!candidate) throw new Error("invalid Stop hook payload");
|
|
81
|
+
const result = await requestDaemon(socket, { method: "turnComplete", candidate }, 3e3);
|
|
82
|
+
if (!result.ok) throw new Error(result.error);
|
|
83
|
+
} catch (error) {
|
|
84
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
85
|
+
const paths = resolveAppPaths();
|
|
86
|
+
await appendFile(paths.logFile, `${(/* @__PURE__ */ new Date()).toISOString()} Stop hook delivery failed: ${message}
|
|
87
|
+
`, { mode: 384 }).catch(() => void 0);
|
|
88
|
+
}
|
|
89
|
+
async function readStdin() {
|
|
90
|
+
process.stdin.setEncoding("utf8");
|
|
91
|
+
let value = "";
|
|
92
|
+
for await (const chunk of process.stdin) value += chunk;
|
|
93
|
+
return value;
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=hook-entry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/hook-entry.ts","../src/agents/stop-event.ts","../src/daemon/client.ts","../src/core/paths.ts"],"sourcesContent":["import { appendFile } from 'node:fs/promises';\nimport { normalizeStopEvent } from './agents/stop-event.js';\nimport { requestDaemon } from './daemon/client.js';\nimport { resolveAppPaths } from './core/paths.js';\n\nconst socket = process.env.LARK_CODING_ASSISTANT_SOCKET;\nconst sessionId = process.env.LARK_CODING_ASSISTANT_SESSION_ID;\n\ntry {\n if (!socket || !sessionId) throw new Error('missing bridge hook environment');\n const payload = JSON.parse(await readStdin()) as unknown;\n const candidate = normalizeStopEvent(sessionId, payload);\n if (!candidate) throw new Error('invalid Stop hook payload');\n const result = await requestDaemon(socket, { method: 'turnComplete', candidate }, 3_000);\n if (!result.ok) throw new Error(result.error);\n} catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n const paths = resolveAppPaths();\n await appendFile(paths.logFile, `${new Date().toISOString()} Stop hook delivery failed: ${message}\\n`, { mode: 0o600 })\n .catch(() => undefined);\n}\n\nasync function readStdin(): Promise<string> {\n process.stdin.setEncoding('utf8');\n let value = '';\n for await (const chunk of process.stdin) value += chunk;\n return value;\n}\n","import type { TurnCompleteCandidate } from './types.js';\n\nexport function normalizeStopEvent(\n bridgeSessionId: string,\n value: unknown,\n): TurnCompleteCandidate | undefined {\n if (!bridgeSessionId || !value || typeof value !== 'object') return undefined;\n const event = value as Record<string, unknown>;\n if (event.hook_event_name !== 'Stop') return undefined;\n const eventId = stringField(event, 'turn_id') ?? stringField(event, 'prompt_id');\n const agentSessionId = stringField(event, 'session_id');\n const cwd = stringField(event, 'cwd');\n const lastAssistantMessage = stringField(event, 'last_assistant_message')?.trim();\n if (!eventId || !agentSessionId || !cwd || !lastAssistantMessage) return undefined;\n return { sessionId: bridgeSessionId, eventId, agentSessionId, cwd, lastAssistantMessage };\n}\n\nexport function validTurnCompleteCandidate(value: unknown): value is TurnCompleteCandidate {\n if (!value || typeof value !== 'object') return false;\n const candidate = value as Record<string, unknown>;\n return ['sessionId', 'eventId', 'agentSessionId', 'cwd', 'lastAssistantMessage']\n .every((key) => typeof candidate[key] === 'string' && (candidate[key] as string).length > 0);\n}\n\nfunction stringField(value: Record<string, unknown>, key: string): string | undefined {\n const field = value[key];\n return typeof field === 'string' && field.length > 0 ? field : undefined;\n}\n","import { randomUUID } from 'node:crypto';\nimport { createConnection } from 'node:net';\nimport type { DaemonRequestInput, DaemonResult } from './protocol.js';\n\nexport function requestDaemon(\n socketPath: string,\n request: DaemonRequestInput,\n timeoutMs = 5000,\n): Promise<DaemonResult> {\n return new Promise((resolve, reject) => {\n const id = randomUUID();\n const socket = createConnection(socketPath);\n let buffer = '';\n const timer = setTimeout(() => {\n socket.destroy();\n reject(new Error('daemon request timed out'));\n }, timeoutMs);\n socket.setEncoding('utf8');\n socket.once('connect', () => socket.write(`${JSON.stringify({ ...request, id })}\\n`));\n socket.on('data', (chunk) => {\n buffer += chunk;\n const newline = buffer.indexOf('\\n');\n if (newline === -1) return;\n clearTimeout(timer);\n socket.destroy();\n try {\n resolve(JSON.parse(buffer.slice(0, newline)) as DaemonResult);\n } catch (error) {\n reject(error);\n }\n });\n socket.once('error', (error) => {\n clearTimeout(timer);\n reject(error);\n });\n });\n}\n","import { homedir } from 'node:os';\nimport { join } from 'node:path';\n\nexport interface AppPaths {\n root: string;\n config: string;\n secrets: string;\n state: string;\n logsDir: string;\n logFile: string;\n runtimeDir: string;\n socket: string;\n pid: string;\n}\n\nexport function resolveAppPaths(root = process.env.LARK_CODING_ASSISTANT_HOME): AppPaths {\n const base = root || join(homedir(), '.lark-coding-assistant');\n return {\n root: base,\n config: join(base, 'config.json'),\n secrets: join(base, 'secrets.json'),\n state: join(base, 'state.json'),\n logsDir: join(base, 'logs'),\n logFile: join(base, 'logs', 'assistant.log'),\n runtimeDir: join(base, 'runtime'),\n socket: join(base, 'runtime', 'daemon.sock'),\n pid: join(base, 'runtime', 'daemon.pid'),\n };\n}\n"],"mappings":"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","names":["socket"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "lark-coding-assistant",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Control local coding-agent CLI sessions from Feishu or Lark",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"lark-coding-assistant": "bin/lark-coding-assistant.mjs",
|
|
8
|
+
"lark-coding-assistant-hook": "bin/lark-coding-assistant-hook.mjs"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"bin",
|
|
13
|
+
"README.md",
|
|
14
|
+
"LICENSE"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsup",
|
|
18
|
+
"typecheck": "tsc --noEmit",
|
|
19
|
+
"test": "vitest run",
|
|
20
|
+
"dev": "tsx src/cli.ts",
|
|
21
|
+
"prepack": "npm run build",
|
|
22
|
+
"prepublishOnly": "npm run typecheck && npm test"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@clack/prompts": "^1.4.0",
|
|
26
|
+
"@larksuite/channel": "^0.4.1",
|
|
27
|
+
"commander": "^12.1.0",
|
|
28
|
+
"qrcode-terminal": "^0.12.0"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/node": "^22.10.0",
|
|
32
|
+
"@types/qrcode-terminal": "^0.12.2",
|
|
33
|
+
"tsx": "^4.20.0",
|
|
34
|
+
"tsup": "^8.3.5",
|
|
35
|
+
"typescript": "^5.6.3",
|
|
36
|
+
"vitest": "^4.1.11"
|
|
37
|
+
},
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=20.12.0"
|
|
40
|
+
},
|
|
41
|
+
"repository": {
|
|
42
|
+
"type": "git",
|
|
43
|
+
"url": "git+https://github.com/YeFeng-Silence/lark-coding-assistant.git"
|
|
44
|
+
},
|
|
45
|
+
"bugs": {
|
|
46
|
+
"url": "https://github.com/YeFeng-Silence/lark-coding-assistant/issues"
|
|
47
|
+
},
|
|
48
|
+
"homepage": "https://github.com/YeFeng-Silence/lark-coding-assistant#readme",
|
|
49
|
+
"author": "YeFeng-Silence",
|
|
50
|
+
"publishConfig": {
|
|
51
|
+
"access": "public",
|
|
52
|
+
"registry": "https://registry.npmjs.org"
|
|
53
|
+
},
|
|
54
|
+
"license": "MIT"
|
|
55
|
+
}
|