@foxden-app/foxclaw 0.3.8 → 0.3.10

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.
@@ -4617,8 +4617,8 @@ export class BridgeSessionCore {
4617
4617
  };
4618
4618
  const text = formatThreadsMessage(locale, threadLikes, binding.threadId, listState.searchTerm, listState);
4619
4619
  const keyboard = parseWeixinBridgeScope(scopeId)
4620
- ? buildThreadsKeyboard(locale, threadLikes)
4621
- : buildThreadListKeyboard(locale, threadLikes, listState);
4620
+ ? buildThreadsKeyboard(locale, threadLikes, binding.threadId)
4621
+ : buildThreadListKeyboard(locale, threadLikes, listState, binding.threadId);
4622
4622
  await this.editHtmlMessage(scopeId, event.messageId, text, keyboard);
4623
4623
  }
4624
4624
  let callbackText = t(locale, 'thread_opened');
@@ -5058,13 +5058,14 @@ export class BridgeSessionCore {
5058
5058
  threadId: row.threadId,
5059
5059
  name: row.name,
5060
5060
  preview: row.preview,
5061
+ cwd: row.cwd,
5061
5062
  archived: row.archived,
5062
5063
  }));
5063
5064
  text += `\n\n${formatWeixinThreadsCopyPaste(locale, rows, searchTerm ?? null, offset)}`;
5064
5065
  }
5065
5066
  const keyboard = parseWeixinBridgeScope(scopeId)
5066
- ? buildThreadsKeyboard(locale, forDisplay)
5067
- : buildThreadListKeyboard(locale, forDisplay, presentationState);
5067
+ ? buildThreadsKeyboard(locale, forDisplay, binding?.threadId ?? null)
5068
+ : buildThreadListKeyboard(locale, forDisplay, presentationState, binding?.threadId ?? null);
5068
5069
  if (messageId !== undefined) {
5069
5070
  await this.editHtmlMessage(scopeId, messageId, text, keyboard);
5070
5071
  return;
@@ -33,12 +33,12 @@ export interface ThreadListPresentationState {
33
33
  archived?: boolean;
34
34
  }
35
35
  export declare function formatThreadsMessage(locale: AppLocale, threads: ThreadLike[], currentThreadId: string | null, searchTerm?: string | null, listState?: ThreadListPresentationState): string;
36
- export declare function buildThreadsKeyboard(locale: AppLocale, threads: ThreadLike[]): Array<Array<{
36
+ export declare function buildThreadsKeyboard(locale: AppLocale, threads: ThreadLike[], currentThreadId?: string | null): Array<Array<{
37
37
  text: string;
38
38
  callback_data: string;
39
39
  }>>;
40
40
  /** Threads keyboard plus Prev/Next and optional clear-filter row (Telegram only). */
41
- export declare function buildThreadListKeyboard(locale: AppLocale, threads: ThreadLike[], listState: ThreadListPresentationState): Array<Array<{
41
+ export declare function buildThreadListKeyboard(locale: AppLocale, threads: ThreadLike[], listState: ThreadListPresentationState, currentThreadId?: string | null): Array<Array<{
42
42
  text: string;
43
43
  callback_data: string;
44
44
  }>>;
@@ -51,6 +51,7 @@ export interface WeixinCopyPasteThreadRow {
51
51
  threadId: string;
52
52
  name: string | null;
53
53
  preview: string;
54
+ cwd?: string | null;
54
55
  archived?: boolean;
55
56
  }
56
57
  export declare function formatWeixinThreadsCopyPaste(locale: AppLocale, threads: WeixinCopyPasteThreadRow[], searchTerm?: string | null,
@@ -24,7 +24,7 @@ export function formatThreadsMessage(locale, threads, currentThreadId, searchTer
24
24
  }));
25
25
  }
26
26
  if (currentThread) {
27
- const currentTitle = truncate(compactWhitespace(currentThread.name || currentThread.preview || t(locale, 'empty')), 40);
27
+ const currentTitle = truncate(formatThreadDisplayName(locale, currentThread), 40);
28
28
  headerLines.push(t(locale, 'threads_current', { title: escapeTelegramHtml(currentTitle) }));
29
29
  headerLines.push(escapeTelegramHtml([
30
30
  formatCwd(locale, currentThread.cwd),
@@ -32,13 +32,16 @@ export function formatThreadsMessage(locale, threads, currentThreadId, searchTer
32
32
  formatStatusLabel(locale, currentThread.status),
33
33
  ].filter(Boolean).join(' | ')));
34
34
  }
35
+ headerLines.push('', ...threads.map((thread, index) => formatThreadListLine(locale, thread, currentThreadId, index)));
35
36
  return headerLines.join('\n');
36
37
  }
37
- export function buildThreadsKeyboard(locale, threads) {
38
+ export function buildThreadsKeyboard(locale, threads, currentThreadId = null) {
38
39
  return threads.flatMap((thread, index) => {
39
40
  const ordinal = typeof thread.index === 'number' ? thread.index : index + 1;
41
+ const selected = currentThreadId === thread.threadId;
42
+ const prefix = selected ? '✅ ' : '';
40
43
  const openRow = [{
41
- text: `🧵 ${ordinal}. ${truncate(compactWhitespace(thread.name || thread.preview || t(locale, 'empty')), 28)}`,
44
+ text: `${prefix}${ordinal}. ${truncate(formatThreadDisplayName(locale, thread), 48)}`,
42
45
  callback_data: `thread:open:${thread.threadId}`,
43
46
  }];
44
47
  const actionRow = thread.archived
@@ -52,9 +55,15 @@ export function buildThreadsKeyboard(locale, threads) {
52
55
  return [openRow, actionRow];
53
56
  });
54
57
  }
58
+ function formatThreadListLine(locale, thread, currentThreadId, index) {
59
+ const ordinal = typeof thread.index === 'number' ? thread.index : index + 1;
60
+ const prefix = currentThreadId === thread.threadId ? '✅ ' : '';
61
+ const title = truncate(formatThreadDisplayName(locale, thread), 72);
62
+ return `${prefix}${ordinal}. ${escapeTelegramHtml(title)}`;
63
+ }
55
64
  /** Threads keyboard plus Prev/Next and optional clear-filter row (Telegram only). */
56
- export function buildThreadListKeyboard(locale, threads, listState) {
57
- const rows = buildThreadsKeyboard(locale, threads);
65
+ export function buildThreadListKeyboard(locale, threads, listState, currentThreadId = null) {
66
+ const rows = buildThreadsKeyboard(locale, threads, currentThreadId);
58
67
  rows.push([{ text: t(locale, 'button_new_thread'), callback_data: 'thread:new' }]);
59
68
  const navigationRow = [];
60
69
  if (listState.hasPreviousPage) {
@@ -142,7 +151,7 @@ pageOffset = 0) {
142
151
  for (let i = 0; i < threads.length; i += 1) {
143
152
  const index = pageOffset + i + 1;
144
153
  const thread = threads[i];
145
- const title = truncate(compactWhitespace(thread.name || thread.preview || t(locale, 'empty')), 40);
154
+ const title = truncate(formatThreadDisplayName(locale, thread), 40);
146
155
  lines.push(`${index}. ${title}`);
147
156
  if (thread.archived) {
148
157
  lines.push(`/thread_unarchive ${index}`);
@@ -534,6 +543,11 @@ function formatCwd(locale, cwd) {
534
543
  const base = path.basename(cwd);
535
544
  return base || cwd;
536
545
  }
546
+ function formatThreadDisplayName(locale, thread) {
547
+ const folder = compactWhitespace(formatCwd(locale, thread.cwd ?? null));
548
+ const title = compactWhitespace(thread.name || thread.preview || t(locale, 'empty'));
549
+ return `${folder}|${title}`;
550
+ }
537
551
  function compactWhitespace(value) {
538
552
  return value.replace(/\s+/g, ' ').trim();
539
553
  }
package/dist/main.js CHANGED
@@ -8,7 +8,8 @@ import { fileURLToPath } from 'node:url';
8
8
  import { APP_HOME, DEFAULT_ENV_PATH, DEFAULT_LOG_PATH, DEFAULT_STATUS_PATH, getLoadedEnvPath, loadConfig, loadEnv, } from './config.js';
9
9
  import { acquireProcessLock, LockHeldError } from './lock.js';
10
10
  import { readRuntimeStatus, writeRuntimeStatus } from './runtime.js';
11
- const command = process.argv[2] || 'serve';
11
+ const rawCommand = process.argv[2];
12
+ const command = rawCommand || 'serve';
12
13
  loadEnv();
13
14
  const packageRoot = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
14
15
  const entryPoint = fileURLToPath(import.meta.url);
@@ -23,6 +24,14 @@ const PROXY_ENV_KEYS = [
23
24
  'no_proxy',
24
25
  ];
25
26
  async function main() {
27
+ if (isVersionCommand(command)) {
28
+ console.log(readPackageVersion());
29
+ return;
30
+ }
31
+ if (isHelpCommand(command)) {
32
+ printUsage();
33
+ return;
34
+ }
26
35
  if (command === 'init') {
27
36
  await initConfig();
28
37
  return;
@@ -73,9 +82,44 @@ async function main() {
73
82
  await runWeixinLoginCli();
74
83
  return;
75
84
  }
85
+ if (command !== 'serve') {
86
+ console.error(`Unknown command: ${command}`);
87
+ printUsage();
88
+ process.exit(1);
89
+ }
76
90
  requireNode24(command);
77
91
  await runServeCli();
78
92
  }
93
+ function isVersionCommand(value) {
94
+ return value === 'version' || value === '--version' || value === '-v' || value === '-V';
95
+ }
96
+ function isHelpCommand(value) {
97
+ return value === 'help' || value === '--help' || value === '-h';
98
+ }
99
+ function readPackageVersion() {
100
+ try {
101
+ const pkg = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8'));
102
+ return typeof pkg.version === 'string' && pkg.version.trim() ? pkg.version : '0.0.0';
103
+ }
104
+ catch {
105
+ return '0.0.0';
106
+ }
107
+ }
108
+ function printUsage() {
109
+ console.log(`FoxClaw ${readPackageVersion()}
110
+
111
+ Usage:
112
+ foxclaw [serve]
113
+ foxclaw init
114
+ foxclaw doctor
115
+ foxclaw status
116
+ foxclaw start|restart|stop
117
+ foxclaw install-systemd|uninstall-systemd
118
+ foxclaw install-launchd
119
+ foxclaw weixin-login [account-id]
120
+ foxclaw --version
121
+ foxclaw --help`);
122
+ }
79
123
  async function runServeCli() {
80
124
  const [{ BridgeMessagingRouter }, { TelegramMessagingPort }, { WeixinChannelAdapter }, { WeixinMessagingPort }, { attachIlinkRuntimeFromBridgeLogger }, { loadWeixinAccount }, { Logger }, { BridgeStore }, { TelegramGateway }, { CodexAppClient }, { BridgeSessionCore }, { TelegramChannelAdapter },] = await Promise.all([
81
125
  import('./channels/bridge_messaging_router.js'),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@foxden-app/foxclaw",
3
- "version": "0.3.8",
3
+ "version": "0.3.10",
4
4
  "description": "Foxden local execution claw for controlling Codex from trusted chat interfaces.",
5
5
  "type": "module",
6
6
  "main": "dist/main.js",
@@ -49,7 +49,7 @@
49
49
  "install-launchd": "node dist/main.js install-launchd",
50
50
  "typecheck": "tsc --noEmit",
51
51
  "lint": "eslint .",
52
- "test": "node --test --import tsx \"src/**/*.test.ts\"",
52
+ "test": "node --test --import tsx $(find src -name '*.test.ts' -print)",
53
53
  "prepack": "npm run build"
54
54
  },
55
55
  "dependencies": {
@@ -1,6 +1,6 @@
1
1
  ---
2
- name: foxclaw
3
- description: Deploy, configure, and validate FoxClaw on macOS, locally or over SSH. Use when Codex needs to clone or update the FoxClaw repo, collect Telegram bot and chat/topic values, write `.env`, enable a launchd service, and guide the user through the first Telegram message test. Trigger on requests about trusted chat interfaces controlling local Codex, copying FoxClaw to another Mac, remote FoxClaw deployment, or turning this repo into an installable Codex skill.
2
+ name: foxclaw
3
+ description: Deploy, configure, validate, develop, and release FoxClaw. Use when Codex needs to clone or update the FoxClaw repo, collect Telegram values, write `.env`, enable launchd/systemd, guide first-message tests, or perform FoxClaw repo wrap-up actions such as Chinese commit messages, push, npm publish, and local install/service update.
4
4
  ---
5
5
 
6
6
  # FoxClaw
@@ -158,10 +158,47 @@ Do this whenever the bridge has been started:
158
158
  - privacy mode still on
159
159
  - bot not admin in group
160
160
 
161
- Do not describe the setup as "done" until this smoke test has either passed or been blocked by missing Telegram-side access.
162
-
163
- ## Resources
164
-
165
- - [references/telegram-setup.md](./references/telegram-setup.md): Telegram-side checklist and ID discovery
161
+ Do not describe the setup as "done" until this smoke test has either passed or been blocked by missing Telegram-side access.
162
+
163
+ ## Development Wrap-Up
164
+
165
+ Use this checklist when the user asks for standard closing actions, release wrap-up, local install updates, npm publish, or says things like "收尾动作", "中文 commit msg", "push", "npm publish", or "本地安装更新".
166
+
167
+ 1. Inspect scope before staging:
168
+ - `git status -sb`
169
+ - `git diff --stat`
170
+ - `git diff --name-status`
171
+ 2. Run the relevant verification with Node 24+. If the system `node` is older, prepend the known Node 24 bin path or use the repo's documented Node 24 shell.
172
+ - Code changes: `npm run typecheck`, `npm run lint`, `npm test`
173
+ - Package/release changes: also run `npm pack --dry-run`
174
+ - Skill-only changes: validate the skill folder, then run `npm pack --dry-run`
175
+ 3. Build before any local service restart:
176
+ - `npm run build`
177
+ 4. Commit intentionally:
178
+ - Stage only the changed files that belong to the task.
179
+ - Use a Chinese commit message when the user asked in Chinese or explicitly said "中文 commit msg".
180
+ - Never stage unrelated local changes.
181
+ 5. Push the current branch after a successful commit:
182
+ - `git push origin <branch>`
183
+ 6. Refresh the local install when requested:
184
+ - If the user has a pnpm global FoxClaw install, prefer `pnpm add -g <repo-path>` so the global `foxclaw` points at the local repo.
185
+ - Rebuild before restarting because local linked installs run `dist/main.js`.
186
+ - Refresh systemd with the existing service env path, for example `FOXCLAW_ENV=<existing-env> <node24> dist/main.js install-systemd`. Do not run `install-systemd` from the repo without `FOXCLAW_ENV`, because it may rewrite the service to use the repo `.env`.
187
+ - For macOS launchd, use the launchd install/start path from this skill and verify with `node dist/main.js status`.
188
+ - Verify the running service reports the expected FoxClaw version in `status`.
189
+ - If `doctor` fails only because `DEFAULT_CWD` is missing, report that separately; do not treat it as evidence that the service update failed.
190
+ 7. Publish to npm when requested:
191
+ - Prefer GitHub Actions trusted publishing via `.github/workflows/publish.yml`: bump and commit the package version, push `main`, then push a matching `v<version>` tag. The tag version must match `package.json`.
192
+ - Configure npmjs.com trusted publishing for `foxden-app/foxclaw` and workflow `publish.yml`; do not store npm tokens if OIDC trusted publishing is available.
193
+ - Temporary fallback: store an npm automation/bypass-2FA token as the GitHub Actions secret `NPM_TOKEN`. Never print the token or commit it.
194
+ - Check `npm whoami` and `npm view @foxden-app/foxclaw version`.
195
+ - If the target version is already published, bump with `npm version patch --no-git-tag-version` before validation and commit.
196
+ - Manual fallback only: run `BROWSER=true npm publish` in a TTY.
197
+ - If npm returns `ENEEDAUTH`, report that npm publish is blocked by registry login and leave the package unreported as published.
198
+ - Never print npm tokens or `.npmrc` auth values.
199
+
200
+ ## Resources
201
+
202
+ - [references/telegram-setup.md](./references/telegram-setup.md): Telegram-side checklist and ID discovery
166
203
  - `scripts/bootstrap_host.py`: install and configure the bridge on the current Mac
167
204
  - `scripts/bootstrap_remote.py`: run the same bootstrap on another Mac over SSH
@@ -1,4 +1,4 @@
1
- interface:
2
- display_name: "FoxClaw"
3
- short_description: "Install, configure, and validate Telegram Codex bridges"
4
- default_prompt: "Use $foxclaw to clone the FoxClaw repo, collect Telegram values, write .env, start the service, and verify it with a first Telegram message."
1
+ interface:
2
+ display_name: "FoxClaw"
3
+ short_description: "Install, validate, update, and release FoxClaw bridges"
4
+ default_prompt: "Use $foxclaw to clone or update FoxClaw, configure Telegram values, validate service health, or finish repo changes with commit, push, npm publish, and local install refresh."
@@ -5,7 +5,7 @@ description: Publish npm packages safely, especially packages that require npm 2
5
5
 
6
6
  # NPM Publish
7
7
 
8
- Use this skill to publish an npm package from a repo. Prefer the normal no-2FA publish path first. Fall back to npm web-auth only when npm explicitly prompts for it.
8
+ Use this skill to publish an npm package from a repo. Prefer CI trusted publishing when the repository has a publish workflow. Prefer the normal no-2FA publish path first only when CI publishing is not available. Fall back to npm web-auth only when npm explicitly prompts for it.
9
9
 
10
10
  ## Release Checklist
11
11
 
@@ -43,6 +43,40 @@ Use this skill to publish an npm package from a repo. Prefer the normal no-2FA p
43
43
 
44
44
  ## Publish Flow
45
45
 
46
+ ### GitHub Actions Trusted Publishing
47
+
48
+ Use this path when the repo has `.github/workflows/publish.yml` and npmjs.com has a trusted publisher configured for the package.
49
+
50
+ 1. Verify the package version is not already published:
51
+ ```bash
52
+ npm view <package-name> version
53
+ ```
54
+
55
+ 2. If needed, bump the package version and commit it before tagging:
56
+ ```bash
57
+ npm version patch --no-git-tag-version
58
+ git add package.json package-lock.json
59
+ git commit -m "<Chinese release message when appropriate>"
60
+ git push origin <branch>
61
+ ```
62
+
63
+ 3. Publish by pushing a matching version tag:
64
+ ```bash
65
+ git tag v<package-version>
66
+ git push origin v<package-version>
67
+ ```
68
+
69
+ 4. Watch the GitHub Actions `Publish` workflow. It must run lint, typecheck, tests, `npm pack --dry-run`, then `npm publish --access public` through trusted publishing.
70
+
71
+ 5. Verify the registry after the workflow succeeds:
72
+ ```bash
73
+ npm view <package-name> version
74
+ ```
75
+
76
+ Do not store or print npm tokens when trusted publishing is available. If trusted publishing is not configured on npmjs.com, the workflow can use a GitHub Actions secret named `NPM_TOKEN` as a temporary fallback. Store only automation/bypass-2FA tokens there, never paste tokens in chat or commit them.
77
+
78
+ ### Manual Publish
79
+
46
80
  1. Start publish in a TTY and disable local browser opening. This works both when npm publishes directly and when it asks for web auth:
47
81
  ```bash
48
82
  BROWSER=true npm publish