codelark 0.1.0

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 (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +193 -0
  3. package/SECURITY.md +34 -0
  4. package/SKILL.md +67 -0
  5. package/agents/openai.yaml +4 -0
  6. package/dist/cli.mjs +8794 -0
  7. package/dist/daemon.mjs +47172 -0
  8. package/dist/ui-server.mjs +22165 -0
  9. package/package.json +73 -0
  10. package/schemas/config.v1.schema.json +259 -0
  11. package/schemas/data/audit.v1.schema.json +44 -0
  12. package/schemas/data/auto-tasks.v1.schema.json +94 -0
  13. package/schemas/data/channel-chats.v1.schema.json +159 -0
  14. package/schemas/data/channel-default-targets.v1.schema.json +43 -0
  15. package/schemas/data/messages.v1.schema.json +23 -0
  16. package/schemas/data/number-map.v1.schema.json +9 -0
  17. package/schemas/data/permissions.v1.schema.json +35 -0
  18. package/schemas/data/sessions.v1.schema.json +330 -0
  19. package/schemas/data/string-map.v1.schema.json +9 -0
  20. package/schemas/manifest.json +121 -0
  21. package/scripts/analyze-bridge-log.js +838 -0
  22. package/scripts/build-preflight.d.ts +21 -0
  23. package/scripts/build-preflight.js +70 -0
  24. package/scripts/build.js +53 -0
  25. package/scripts/check-npm-pack.js +46 -0
  26. package/scripts/daemon.ps1 +16 -0
  27. package/scripts/daemon.sh +206 -0
  28. package/scripts/doctor.ps1 +27 -0
  29. package/scripts/doctor.sh +185 -0
  30. package/scripts/hot-update-bridge.sh +298 -0
  31. package/scripts/install-codex-skills.sh +127 -0
  32. package/scripts/install-codex.sh +10 -0
  33. package/scripts/migrate-bindings-to-channel-chats.js +228 -0
  34. package/scripts/patch-codex-sdk-windows-hide.js +96 -0
  35. package/scripts/real-feishu-e2e.ts +5804 -0
  36. package/scripts/run-tests.js +83 -0
  37. package/scripts/setup-wizard-real-e2e.ts +195 -0
  38. package/scripts/supervisor-linux.sh +49 -0
  39. package/scripts/supervisor-macos.sh +167 -0
  40. package/scripts/supervisor-windows.ps1 +481 -0
  41. package/skills/codelark/SKILL.md +67 -0
  42. package/skills/codelark-auto/SKILL.md +80 -0
  43. package/skills/codelark-question/SKILL.md +54 -0
@@ -0,0 +1,298 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ PROJECT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
5
+ CODELARK_HOME="${CODELARK_HOME:-$HOME/.codelark}"
6
+ LOG_DIR="$CODELARK_HOME/logs"
7
+ BRIDGE_LOG="$LOG_DIR/bridge.log"
8
+
9
+ usage() {
10
+ cat <<'USAGE'
11
+ Usage: bash scripts/hot-update-bridge.sh [--pull] [--skip-tests] [--dry-run] [--run]
12
+
13
+ Dispatch a detached CodeLark hot update so the current bridge-hosted
14
+ Codex session can survive the bridge stop/start sequence.
15
+
16
+ Options:
17
+ --pull Run git pull before build/test/restart.
18
+ --skip-tests Skip npm test during this hot update.
19
+ --dry-run Validate environment and print the planned detached update
20
+ without dispatching a worker, building, testing, or restarting.
21
+ --run Internal worker mode. Do not call directly from a bridge session.
22
+ USAGE
23
+ }
24
+
25
+ USE_PULL=0
26
+ SKIP_TESTS=0
27
+ RUN_WORKER=0
28
+ DRY_RUN=0
29
+
30
+ while [ "$#" -gt 0 ]; do
31
+ case "$1" in
32
+ --pull)
33
+ USE_PULL=1
34
+ ;;
35
+ --skip-tests)
36
+ SKIP_TESTS=1
37
+ ;;
38
+ --run)
39
+ RUN_WORKER=1
40
+ ;;
41
+ --dry-run)
42
+ DRY_RUN=1
43
+ ;;
44
+ -h|--help)
45
+ usage
46
+ exit 0
47
+ ;;
48
+ *)
49
+ echo "Unknown option: $1" >&2
50
+ usage >&2
51
+ exit 2
52
+ ;;
53
+ esac
54
+ shift
55
+ done
56
+
57
+ validate_project_dir() {
58
+ if [ ! -f "$PROJECT_DIR/package.json" ] || [ ! -f "$PROJECT_DIR/scripts/hot-update-bridge.sh" ]; then
59
+ echo "[hot-update] refusing to run outside a CodeLark/codelark project directory" >&2
60
+ exit 1
61
+ fi
62
+
63
+ local package_name
64
+ package_name="$(env -u NODE_OPTIONS node -e "const fs = require('fs'); const pkg = JSON.parse(fs.readFileSync(process.argv[1], 'utf8')); process.stdout.write(String(pkg.name || ''));" "$PROJECT_DIR/package.json" 2>/dev/null || true)"
65
+ if [ "$package_name" != "codelark" ]; then
66
+ echo "[hot-update] refusing to run outside a CodeLark/codelark project directory" >&2
67
+ exit 1
68
+ fi
69
+ }
70
+
71
+ bridge_cli_display() {
72
+ if [ -f "$PROJECT_DIR/dist/cli.mjs" ]; then
73
+ echo "node $PROJECT_DIR/dist/cli.mjs"
74
+ return
75
+ fi
76
+ if command -v codelark >/dev/null 2>&1; then
77
+ echo "codelark"
78
+ return
79
+ fi
80
+ echo "codelark"
81
+ }
82
+
83
+ run_bridge_cli() {
84
+ if [ -f "$PROJECT_DIR/dist/cli.mjs" ]; then
85
+ node "$PROJECT_DIR/dist/cli.mjs" "$@"
86
+ return
87
+ fi
88
+ if command -v codelark >/dev/null 2>&1; then
89
+ codelark "$@"
90
+ return
91
+ fi
92
+ echo "[hot-update] codelark CLI not found" >&2
93
+ return 127
94
+ }
95
+
96
+ ensure_node24() {
97
+ # npm test can export npm_config_prefix, which makes nvm refuse to run.
98
+ # Hot update owns its Node runtime selection, so clear the incompatible prefix.
99
+ unset npm_config_prefix NPM_CONFIG_PREFIX
100
+
101
+ # Check if Node 24 is already available in the current environment.
102
+ # This handles CI environments (GitHub Actions, etc.) where Node is installed
103
+ # via setup-node rather than nvm.
104
+ if command -v node >/dev/null 2>&1; then
105
+ local current_major
106
+ current_major="$(node -p "process.versions.node.split('.')[0]" 2>/dev/null || true)"
107
+ if [ "$current_major" = "24" ]; then
108
+ return
109
+ fi
110
+ fi
111
+
112
+ # Try to switch to Node 24 via nvm if available.
113
+ local nvm_root="${NVM_DIR:-$HOME/.nvm}"
114
+ if [ -s "$nvm_root/nvm.sh" ]; then
115
+ # shellcheck source=/dev/null
116
+ source "$nvm_root/nvm.sh"
117
+ # Allow nvm use to fail gracefully if Node 24 is not installed via nvm.
118
+ # We'll check again below and search for a manually-installed Node 24.
119
+ nvm use 24 >/dev/null 2>&1 || true
120
+ fi
121
+
122
+ # Check again after attempting nvm use.
123
+ if command -v node >/dev/null 2>&1; then
124
+ local current_major
125
+ current_major="$(node -p "process.versions.node.split('.')[0]" 2>/dev/null || true)"
126
+ if [ "$current_major" = "24" ]; then
127
+ return
128
+ fi
129
+ fi
130
+
131
+ # Search for Node 24 in common nvm installation directories.
132
+ local node24=""
133
+ local root
134
+ for root in \
135
+ "${NVM_DIR:-}" \
136
+ "$HOME/.nvm" \
137
+ "/home/${USER:-}/.nvm" \
138
+ "/data00/home/${USER:-}/.nvm"
139
+ do
140
+ [ -n "$root" ] || continue
141
+ [ -d "$root/versions/node" ] || continue
142
+ local candidate
143
+ for candidate in "$root"/versions/node/v24.*/bin/node; do
144
+ [ -x "$candidate" ] || continue
145
+ node24="$candidate"
146
+ done
147
+ done
148
+ if [ -n "$node24" ]; then
149
+ PATH="$(dirname "$node24"):$PATH"
150
+ export PATH
151
+ fi
152
+
153
+ # Final check: ensure Node 24 is now available.
154
+ if ! command -v node >/dev/null 2>&1; then
155
+ echo "[hot-update] node is not available in PATH" >&2
156
+ exit 1
157
+ fi
158
+
159
+ local major
160
+ major="$(node -p "process.versions.node.split('.')[0]")"
161
+ if [ "$major" != "24" ]; then
162
+ echo "[hot-update] Node.js 24 is required, found $(node -v)" >&2
163
+ exit 1
164
+ fi
165
+ }
166
+
167
+ node_supports_env_proxy() {
168
+ node --help 2>/dev/null | grep -F -- --use-env-proxy >/dev/null 2>&1
169
+ }
170
+
171
+ run_logged() {
172
+ echo "[hot-update] $*"
173
+ "$@"
174
+ }
175
+
176
+ run_worker() {
177
+ cd "$PROJECT_DIR"
178
+ mkdir -p "$LOG_DIR"
179
+
180
+ echo "[hot-update] started $(date -Is)"
181
+ echo "[hot-update] project: $PROJECT_DIR"
182
+ echo "[hot-update] bridge log: $BRIDGE_LOG"
183
+
184
+ validate_project_dir
185
+
186
+ ensure_node24
187
+ echo "[hot-update] node: $(node -v)"
188
+
189
+ local proxy_supported=0
190
+ if node_supports_env_proxy; then
191
+ proxy_supported=1
192
+ echo "[hot-update] --use-env-proxy: supported"
193
+ else
194
+ echo "[hot-update] --use-env-proxy: not supported"
195
+ fi
196
+
197
+ if [ "$USE_PULL" = "1" ]; then
198
+ run_logged git pull
199
+ else
200
+ echo "[hot-update] git pull: skipped"
201
+ fi
202
+
203
+ run_logged npm run build
204
+ if [ "$SKIP_TESTS" = "1" ]; then
205
+ echo "[hot-update] npm test: skipped by --skip-tests"
206
+ else
207
+ run_logged npm test
208
+ fi
209
+
210
+ local cli
211
+ cli="$(bridge_cli_display)"
212
+ if [ "$proxy_supported" = "1" ]; then
213
+ echo "[hot-update] restart command: NODE_OPTIONS=--use-env-proxy LITELLM_KEY=sk-local-dev $cli stop && NODE_OPTIONS=--use-env-proxy LITELLM_KEY=sk-local-dev $cli start"
214
+ NODE_OPTIONS=--use-env-proxy LITELLM_KEY="sk-local-dev" run_bridge_cli stop
215
+ NODE_OPTIONS=--use-env-proxy LITELLM_KEY="sk-local-dev" run_bridge_cli start
216
+ else
217
+ echo "[hot-update] restart command: LITELLM_KEY=sk-local-dev $cli stop && LITELLM_KEY=sk-local-dev $cli start"
218
+ LITELLM_KEY="sk-local-dev" run_bridge_cli stop
219
+ LITELLM_KEY="sk-local-dev" run_bridge_cli start
220
+ fi
221
+
222
+ echo "[hot-update] completed $(date -Is)"
223
+ }
224
+
225
+ run_dry_run() {
226
+ cd "$PROJECT_DIR"
227
+ validate_project_dir
228
+ ensure_node24
229
+
230
+ local args=(--run)
231
+ if [ "$USE_PULL" = "1" ]; then
232
+ args+=(--pull)
233
+ fi
234
+ if [ "$SKIP_TESTS" = "1" ]; then
235
+ args+=(--skip-tests)
236
+ fi
237
+
238
+ echo "[hot-update] dry-run: yes"
239
+ echo "[hot-update] project: $PROJECT_DIR"
240
+ echo "[hot-update] pwd: $(pwd)"
241
+ echo "[hot-update] script: $PROJECT_DIR/scripts/hot-update-bridge.sh"
242
+ echo "[hot-update] CODELARK_HOME: $CODELARK_HOME"
243
+ echo "[hot-update] log dir: $LOG_DIR"
244
+ echo "[hot-update] bridge log: $BRIDGE_LOG"
245
+ echo "[hot-update] node: $(node -v)"
246
+ if node_supports_env_proxy; then
247
+ echo "[hot-update] --use-env-proxy: supported"
248
+ else
249
+ echo "[hot-update] --use-env-proxy: not supported"
250
+ fi
251
+ echo "[hot-update] worker args: ${args[*]}"
252
+ echo "[hot-update] dispatch command: bash scripts/hot-update-bridge.sh ${args[*]}"
253
+ echo "[hot-update] git pull: $([ "$USE_PULL" = "1" ] && echo planned || echo skipped)"
254
+ echo "[hot-update] npm run build: planned"
255
+ echo "[hot-update] npm test: $([ "$SKIP_TESTS" = "1" ] && echo skipped || echo planned)"
256
+ echo "[hot-update] restart cli: $(bridge_cli_display)"
257
+ echo "[hot-update] restart: planned"
258
+ }
259
+
260
+ dispatch_worker() {
261
+ local log_stamp
262
+ log_stamp="$(date +%Y%m%d-%H%M%S)"
263
+ local log_file="$LOG_DIR/hot-update-$log_stamp.log"
264
+ if ! { mkdir -p "$LOG_DIR" && : >"$log_file"; } 2>/dev/null; then
265
+ local fallback_log_dir="${TMPDIR:-/tmp}/codelark-logs"
266
+ mkdir -p "$fallback_log_dir"
267
+ log_file="$fallback_log_dir/hot-update-$log_stamp.log"
268
+ : >"$log_file"
269
+ fi
270
+ local args=(--run)
271
+ if [ "$USE_PULL" = "1" ]; then
272
+ args+=(--pull)
273
+ fi
274
+ if [ "$SKIP_TESTS" = "1" ]; then
275
+ args+=(--skip-tests)
276
+ fi
277
+
278
+ if command -v setsid >/dev/null 2>&1; then
279
+ nohup setsid bash "$0" "${args[@]}" >"$log_file" 2>&1 </dev/null &
280
+ else
281
+ nohup bash "$0" "${args[@]}" >"$log_file" 2>&1 </dev/null &
282
+ fi
283
+
284
+ echo "Dispatched CodeLark hot update."
285
+ echo "PID: $!"
286
+ echo "Hot update log: $log_file"
287
+ echo "Bridge log: $BRIDGE_LOG"
288
+ echo "Pull requested: $([ "$USE_PULL" = "1" ] && echo yes || echo no)"
289
+ echo "Tests skipped: $([ "$SKIP_TESTS" = "1" ] && echo yes || echo no)"
290
+ }
291
+
292
+ if [ "$DRY_RUN" = "1" ]; then
293
+ run_dry_run
294
+ elif [ "$RUN_WORKER" = "1" ]; then
295
+ run_worker
296
+ else
297
+ dispatch_worker
298
+ fi
@@ -0,0 +1,127 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # Install bundled CodeLark skills and the official Lark lark-doc skill.
5
+ # Usage:
6
+ # bash scripts/install-codex-skills.sh [--link] [skill ...]
7
+ #
8
+ # If no skill name is provided, all default skills are installed. Supported names:
9
+ # codelark IM attachment send-back skill
10
+ # codelark-question explicit question-card skill
11
+ # codelark-auto /auto-script helper skill
12
+ # lark-doc official Lark document skill from larksuite/cli
13
+ #
14
+ # --link only symlinks the primary package skill for local development.
15
+
16
+ CODEX_SKILLS_DIR="$HOME/.codex/skills"
17
+ SOURCE_DIR="$(cd "$(dirname "$0")/.." && pwd)"
18
+ LINK_PRIMARY=0
19
+ REQUESTED_SKILLS=()
20
+
21
+ for arg in "$@"; do
22
+ case "$arg" in
23
+ --link)
24
+ LINK_PRIMARY=1
25
+ ;;
26
+ -h|--help)
27
+ sed -n '3,18p' "$0"
28
+ exit 0
29
+ ;;
30
+ *)
31
+ REQUESTED_SKILLS+=("$arg")
32
+ ;;
33
+ esac
34
+ done
35
+
36
+ if [ "${#REQUESTED_SKILLS[@]}" -eq 0 ]; then
37
+ REQUESTED_SKILLS=(codelark codelark-question codelark-auto lark-doc)
38
+ fi
39
+
40
+ echo "Installing CodeLark skills..."
41
+ echo "Target: $CODEX_SKILLS_DIR"
42
+ echo "Note: lark-doc is installed through the official larksuite/cli skills package."
43
+ echo ""
44
+
45
+ mkdir -p "$CODEX_SKILLS_DIR"
46
+
47
+ skill_source_dir() {
48
+ case "$1" in
49
+ codelark|codelark-auto|codelark-question)
50
+ printf '%s\n' "$SOURCE_DIR/skills/$1"
51
+ ;;
52
+ lark-doc)
53
+ printf '%s\n' ""
54
+ ;;
55
+ *)
56
+ echo "Error: unknown skill '$1'" >&2
57
+ echo "Supported skills: codelark codelark-question codelark-auto lark-doc" >&2
58
+ exit 1
59
+ ;;
60
+ esac
61
+ }
62
+
63
+ install_skill_dir() {
64
+ local name="$1"
65
+ local source_dir="$2"
66
+ local target_dir="$CODEX_SKILLS_DIR/$name"
67
+ if [ ! -f "$source_dir/SKILL.md" ]; then
68
+ echo "Error: SKILL.md not found in $source_dir"
69
+ exit 1
70
+ fi
71
+ if [ -e "$target_dir" ]; then
72
+ if [ -L "$target_dir" ]; then
73
+ local existing
74
+ existing=$(readlink "$target_dir")
75
+ echo "Already installed: $name -> $existing"
76
+ else
77
+ echo "Already installed: $target_dir"
78
+ fi
79
+ return
80
+ fi
81
+ cp -R "$source_dir" "$target_dir"
82
+ echo "Copied $name to: $target_dir"
83
+ }
84
+
85
+ for skill in "${REQUESTED_SKILLS[@]}"; do
86
+ if [ "$skill" = "lark-doc" ]; then
87
+ echo "Installing official lark-doc skill..."
88
+ npx skills add larksuite/cli -s lark-doc -y -g -a claude-code
89
+ continue
90
+ fi
91
+ source_dir="$(skill_source_dir "$skill")"
92
+ target_dir="$CODEX_SKILLS_DIR/$skill"
93
+ if [ "$skill" = "codelark" ] && [ "$LINK_PRIMARY" -eq 1 ]; then
94
+ if [ -e "$target_dir" ]; then
95
+ echo "Already installed: $target_dir"
96
+ else
97
+ ln -s "$source_dir" "$target_dir"
98
+ echo "Symlinked: $target_dir -> $source_dir"
99
+ fi
100
+ else
101
+ install_skill_dir "$skill" "$source_dir"
102
+ fi
103
+ done
104
+
105
+ TARGET_DIR="$CODEX_SKILLS_DIR/codelark"
106
+ if [[ " ${REQUESTED_SKILLS[*]} " == *" codelark "* ]] && [ "$LINK_PRIMARY" -eq 0 ]; then
107
+ if [ ! -d "$TARGET_DIR/node_modules" ] || [ ! -d "$TARGET_DIR/node_modules/@openai/codex-sdk" ]; then
108
+ echo "Installing dependencies for codelark..."
109
+ (cd "$TARGET_DIR" && npm install)
110
+ fi
111
+
112
+ if [ ! -f "$TARGET_DIR/dist/daemon.mjs" ]; then
113
+ echo "Building daemon bundle for codelark..."
114
+ (cd "$TARGET_DIR" && npm run build)
115
+ fi
116
+
117
+ echo "Pruning dev dependencies for codelark..."
118
+ (cd "$TARGET_DIR" && npm prune --production)
119
+ fi
120
+
121
+ if [ "$LINK_PRIMARY" -eq 1 ]; then
122
+ echo ""
123
+ echo "Development mode: no install/build/prune steps were run against the source repo."
124
+ fi
125
+
126
+ echo ""
127
+ echo "Done. Start a new Codex session for newly installed skills to be discoverable."
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
5
+
6
+ echo "scripts/install-codex.sh is kept for compatibility."
7
+ echo "Use scripts/install-codex-skills.sh for the clearer manual CodeLark skill installer."
8
+ echo ""
9
+
10
+ exec "$SCRIPT_DIR/install-codex-skills.sh" "$@"
@@ -0,0 +1,228 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+
6
+ function usage() {
7
+ return [
8
+ 'Usage: node scripts/migrate-bindings-to-channel-chats.js [--codelark-home <path>] [--clk-home <path>] [--dry-run]',
9
+ '',
10
+ 'Migrates data/bindings.json to data/channel-chats.json.',
11
+ 'For each channel/chat pair, keeps the newest active legacy record where active=true and drops inactive records.',
12
+ 'Moves binding workingDirectory/model/mode/chatDisplayName into the linked session when session fields are empty.',
13
+ ].join('\n');
14
+ }
15
+
16
+ function parseArgs(argv) {
17
+ const out = {
18
+ codelarkHome: process.env.CODELARK_HOME || path.join(os.homedir(), '.codelark'),
19
+ dryRun: false,
20
+ };
21
+ for (let i = 0; i < argv.length; i += 1) {
22
+ const arg = argv[i];
23
+ if (arg === '--dry-run') {
24
+ out.dryRun = true;
25
+ } else if (arg === '--codelark-home' || arg === '--clk-home') {
26
+ const value = argv[++i];
27
+ if (!value) throw new Error(`${arg} requires a path`);
28
+ out.codelarkHome = value;
29
+ } else if (arg === '-h' || arg === '--help') {
30
+ console.log(usage());
31
+ process.exit(0);
32
+ } else {
33
+ throw new Error(`Unknown argument: ${arg}`);
34
+ }
35
+ }
36
+ return out;
37
+ }
38
+
39
+ function readJsonObject(filePath) {
40
+ if (!fs.existsSync(filePath)) return {};
41
+ const raw = fs.readFileSync(filePath, 'utf-8');
42
+ const parsed = JSON.parse(raw);
43
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
44
+ throw new Error(`${filePath} must contain a JSON object`);
45
+ }
46
+ return parsed;
47
+ }
48
+
49
+ function atomicWriteJson(filePath, data) {
50
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
51
+ const tmp = `${filePath}.tmp`;
52
+ fs.writeFileSync(tmp, `${JSON.stringify(data, null, 2)}\n`, 'utf-8');
53
+ fs.renameSync(tmp, filePath);
54
+ }
55
+
56
+ function readString(record, key) {
57
+ const value = record?.[key];
58
+ return typeof value === 'string' ? value : '';
59
+ }
60
+
61
+ function readBoolean(record, key) {
62
+ const value = record?.[key];
63
+ return typeof value === 'boolean' ? value : undefined;
64
+ }
65
+
66
+ function normalizeChatKind(value) {
67
+ if (value === 'p2p' || value === 'group') return value;
68
+ return undefined;
69
+ }
70
+
71
+ function updatedTime(record) {
72
+ const parsed = Date.parse(readString(record, 'updatedAt') || readString(record, 'createdAt'));
73
+ return Number.isFinite(parsed) ? parsed : 0;
74
+ }
75
+
76
+ function normalizeMode(value) {
77
+ return value === 'yolo' ? 'yolo' : 'normal';
78
+ }
79
+
80
+ function ensureRecord(parent, key) {
81
+ if (!parent[key] || typeof parent[key] !== 'object' || Array.isArray(parent[key])) {
82
+ parent[key] = {};
83
+ }
84
+ return parent[key];
85
+ }
86
+
87
+ function chooseActiveBinding(records) {
88
+ return records
89
+ .filter((record) => readBoolean(record, 'active') === true)
90
+ .sort((a, b) => updatedTime(b) - updatedTime(a))[0];
91
+ }
92
+
93
+ function toChannelChat(binding) {
94
+ return {
95
+ id: readString(binding, 'id'),
96
+ channelType: readString(binding, 'channelType'),
97
+ ...(readString(binding, 'channelProvider') ? { channelProvider: readString(binding, 'channelProvider') } : {}),
98
+ ...(readString(binding, 'channelAlias') ? { channelAlias: readString(binding, 'channelAlias') } : {}),
99
+ chatId: readString(binding, 'chatId'),
100
+ ...(normalizeChatKind(binding.chatKind) ? { chatKind: normalizeChatKind(binding.chatKind) } : {}),
101
+ ...(readString(binding, 'chatUserId') ? { chatUserId: readString(binding, 'chatUserId') } : {}),
102
+ bridgeSessionId: readString(binding, 'bridgeSessionId'),
103
+ createdAt: readString(binding, 'createdAt') || new Date().toISOString(),
104
+ updatedAt: readString(binding, 'updatedAt') || new Date().toISOString(),
105
+ };
106
+ }
107
+
108
+ function migrateSessionFromBinding(session, binding) {
109
+ let changed = false;
110
+ const workingDirectory = readString(binding, 'workingDirectory');
111
+ const model = readString(binding, 'model');
112
+ const chatDisplayName = readString(binding, 'chatDisplayName');
113
+ const mode = normalizeMode(binding?.mode);
114
+ const runtime = ensureRecord(session, 'runtime');
115
+ const codex = ensureRecord(runtime, 'codex');
116
+ const existingModel = readString(session, 'model');
117
+ const existingMode = readString(session, 'preferred_mode');
118
+
119
+ if (existingModel && !readString(codex, 'model')) {
120
+ codex.model = existingModel;
121
+ changed = true;
122
+ }
123
+ if (existingMode && !readString(codex, 'mode')) {
124
+ codex.mode = normalizeMode(existingMode);
125
+ changed = true;
126
+ }
127
+ if ('model' in session) {
128
+ delete session.model;
129
+ changed = true;
130
+ }
131
+ if ('preferred_mode' in session) {
132
+ delete session.preferred_mode;
133
+ changed = true;
134
+ }
135
+
136
+ if (workingDirectory && !readString(session, 'working_directory')) {
137
+ session.working_directory = workingDirectory;
138
+ changed = true;
139
+ }
140
+ if (model && !readString(codex, 'model')) {
141
+ codex.model = model;
142
+ changed = true;
143
+ }
144
+ if (mode && !readString(codex, 'mode')) {
145
+ codex.mode = mode;
146
+ changed = true;
147
+ }
148
+ if (chatDisplayName && !readString(session, 'name')) {
149
+ session.name = chatDisplayName;
150
+ changed = true;
151
+ }
152
+ if (changed) {
153
+ session.updated_at = readString(session, 'updated_at') || new Date().toISOString();
154
+ }
155
+ return changed;
156
+ }
157
+
158
+ function main() {
159
+ const options = parseArgs(process.argv.slice(2));
160
+ const dataDir = path.join(options.codelarkHome, 'data');
161
+ const bindingsPath = path.join(dataDir, 'bindings.json');
162
+ const channelChatsPath = path.join(dataDir, 'channel-chats.json');
163
+ const sessionsPath = path.join(dataDir, 'sessions.json');
164
+
165
+ const bindings = readJsonObject(bindingsPath);
166
+ const sessions = readJsonObject(sessionsPath);
167
+ const byChat = new Map();
168
+ for (const [key, binding] of Object.entries(bindings)) {
169
+ if (!binding || typeof binding !== 'object') continue;
170
+ const id = readString(binding, 'id') || key;
171
+ const normalized = { ...binding, id };
172
+ const channelType = readString(normalized, 'channelType');
173
+ const chatId = readString(normalized, 'chatId');
174
+ const bridgeSessionId = readString(normalized, 'bridgeSessionId');
175
+ if (!channelType || !chatId || !bridgeSessionId) continue;
176
+ const chatKey = `${channelType}:${chatId}`;
177
+ byChat.set(chatKey, [...(byChat.get(chatKey) || []), normalized]);
178
+ }
179
+
180
+ const channelChats = {};
181
+ let sessionsChanged = 0;
182
+ let droppedBindings = 0;
183
+ let skippedInactiveBindings = 0;
184
+ for (const records of byChat.values()) {
185
+ const kept = chooseActiveBinding(records);
186
+ if (!kept) {
187
+ skippedInactiveBindings += records.length;
188
+ droppedBindings += records.length;
189
+ continue;
190
+ }
191
+ const inactiveCount = records.filter((record) => readBoolean(record, 'active') !== true).length;
192
+ skippedInactiveBindings += inactiveCount;
193
+ droppedBindings += records.length - 1;
194
+ const chat = toChannelChat(kept);
195
+ channelChats[chat.id] = chat;
196
+ const session = sessions[chat.bridgeSessionId];
197
+ if (session && typeof session === 'object' && migrateSessionFromBinding(session, kept)) {
198
+ sessionsChanged += 1;
199
+ }
200
+ }
201
+
202
+ const summary = {
203
+ codelarkHome: options.codelarkHome,
204
+ inputBindings: Object.keys(bindings).length,
205
+ outputChannelChats: Object.keys(channelChats).length,
206
+ droppedBindings,
207
+ skippedInactiveBindings,
208
+ sessionsChanged,
209
+ dryRun: options.dryRun,
210
+ };
211
+
212
+ if (!options.dryRun) {
213
+ atomicWriteJson(channelChatsPath, channelChats);
214
+ atomicWriteJson(sessionsPath, sessions);
215
+ fs.rmSync(bindingsPath, { force: true });
216
+ }
217
+
218
+ console.log(JSON.stringify(summary, null, 2));
219
+ }
220
+
221
+ try {
222
+ main();
223
+ } catch (error) {
224
+ console.error(error instanceof Error ? error.message : String(error));
225
+ console.error('');
226
+ console.error(usage());
227
+ process.exit(1);
228
+ }