sidekick-shared 0.26.4 → 0.26.6

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 (69) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/README.md +59 -51
  3. package/dist/accountChanges.js +21 -2
  4. package/dist/accountHealth.d.ts +72 -0
  5. package/dist/accountHealth.js +286 -0
  6. package/dist/accountLaunch.d.ts +30 -0
  7. package/dist/accountLaunch.js +124 -0
  8. package/dist/accountManager.d.ts +33 -8
  9. package/dist/accountManager.js +183 -50
  10. package/dist/accountRegistry.d.ts +10 -0
  11. package/dist/accountRegistry.js +42 -22
  12. package/dist/accountSwitch.d.ts +43 -0
  13. package/dist/accountSwitch.js +115 -0
  14. package/dist/accountSync.d.ts +30 -0
  15. package/dist/accountSync.js +180 -0
  16. package/dist/accountSyncTypes.d.ts +25 -0
  17. package/dist/accountSyncTypes.js +7 -0
  18. package/dist/accounts.d.ts +58 -6
  19. package/dist/accounts.js +408 -189
  20. package/dist/claudeCredentials.d.ts +38 -0
  21. package/dist/claudeCredentials.js +74 -0
  22. package/dist/claudeProfiles.d.ts +40 -0
  23. package/dist/claudeProfiles.js +102 -10
  24. package/dist/codexAuth.d.ts +32 -0
  25. package/dist/codexAuth.js +127 -0
  26. package/dist/codexPaths.d.ts +21 -0
  27. package/dist/codexPaths.js +117 -0
  28. package/dist/codexProfiles.d.ts +58 -5
  29. package/dist/codexProfiles.js +574 -289
  30. package/dist/credentialIO.d.ts +12 -5
  31. package/dist/credentialIO.js +63 -29
  32. package/dist/credentials.d.ts +8 -0
  33. package/dist/credentials.js +28 -32
  34. package/dist/ensureDefaultAccounts.d.ts +8 -0
  35. package/dist/ensureDefaultAccounts.js +42 -82
  36. package/dist/index.d.ts +31 -12
  37. package/dist/index.js +68 -13
  38. package/dist/keepAlive.d.ts +44 -0
  39. package/dist/keepAlive.js +106 -0
  40. package/dist/node.d.ts +4 -1
  41. package/dist/node.js +4 -1
  42. package/dist/observedSessionCollector.d.ts +117 -5
  43. package/dist/observedSessionCollector.js +483 -76
  44. package/dist/processDetection.d.ts +32 -0
  45. package/dist/processDetection.js +210 -0
  46. package/dist/providers/claudeCode.d.ts +23 -2
  47. package/dist/providers/claudeCode.js +143 -51
  48. package/dist/providers/codex.d.ts +10 -2
  49. package/dist/providers/codex.js +95 -12
  50. package/dist/providers/directoryListingCache.d.ts +85 -0
  51. package/dist/providers/directoryListingCache.js +244 -0
  52. package/dist/providers/rolloutWalker.d.ts +28 -5
  53. package/dist/providers/rolloutWalker.js +100 -16
  54. package/dist/providers/types.d.ts +48 -1
  55. package/dist/schemas/accountManager.d.ts +283 -1
  56. package/dist/schemas/accountManager.js +122 -1
  57. package/dist/schemas/assistantTurn.d.ts +2 -2
  58. package/dist/schemas/index.d.ts +6 -1
  59. package/dist/schemas/index.js +14 -1
  60. package/dist/schemas/observedSessionV1.d.ts +21 -21
  61. package/dist/schemas/quota.d.ts +18 -18
  62. package/dist/schemas/quotaHistory.d.ts +1 -1
  63. package/dist/schemas/stateFile.d.ts +5 -5
  64. package/dist/sessionMonitor.js +24 -2
  65. package/dist/terminalSync.d.ts +5 -5
  66. package/dist/terminalSync.js +7 -112
  67. package/dist/writers/atomic.d.ts +2 -0
  68. package/dist/writers/atomic.js +49 -2
  69. package/package.json +1 -1
@@ -35,6 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.SessionMonitor = void 0;
37
37
  const fs = __importStar(require("node:fs"));
38
+ const path = __importStar(require("node:path"));
38
39
  const EventAggregator_1 = require("./aggregation/EventAggregator");
39
40
  const observedSessionCollector_1 = require("./observedSessionCollector");
40
41
  /**
@@ -119,6 +120,22 @@ class SessionMonitor {
119
120
  ];
120
121
  },
121
122
  read: () => null,
123
+ // One session is watched, so an event naming any other file costs
124
+ // nothing; an event naming this file costs one stat.
125
+ resolveReference: async (signal) => {
126
+ if (!signal.root || typeof signal.filename !== 'string' || !signal.filename) {
127
+ return { status: 'unknown' };
128
+ }
129
+ if (!this.sessionPath)
130
+ return { status: 'ignored' };
131
+ const eventPath = path.resolve(signal.root, signal.filename);
132
+ if (eventPath !== path.resolve(this.sessionPath))
133
+ return { status: 'ignored' };
134
+ const [reference] = await source.discover();
135
+ return reference
136
+ ? { status: 'present', reference }
137
+ : { status: 'missing', sessionId: this.provider.getSessionId(this.sessionPath) };
138
+ },
122
139
  subscribe: (listener, sourceOptions) => {
123
140
  const watchers = new Set();
124
141
  let pollTimer = null;
@@ -131,7 +148,12 @@ class SessionMonitor {
131
148
  }
132
149
  for (const root of new Set(roots)) {
133
150
  try {
134
- const watcher = fs.watch(root, { persistent: false, recursive: true }, listener);
151
+ const watcher = fs.watch(root, { persistent: false, recursive: true }, (eventType, filename) => listener({
152
+ trigger: 'event',
153
+ root,
154
+ eventType,
155
+ filename: filename === null || filename === undefined ? null : String(filename),
156
+ }));
135
157
  watchers.add(watcher);
136
158
  watcher.on('error', () => {
137
159
  watchers.delete(watcher);
@@ -149,7 +171,7 @@ class SessionMonitor {
149
171
  }
150
172
  const pollIntervalMs = sourceOptions?.pollIntervalMs ?? 0;
151
173
  if (pollIntervalMs > 0) {
152
- pollTimer = setInterval(listener, pollIntervalMs);
174
+ pollTimer = setInterval(() => listener({ trigger: 'poll' }), pollIntervalMs);
153
175
  pollTimer.unref?.();
154
176
  }
155
177
  return {
@@ -1,7 +1,7 @@
1
- import { type AccountProviderId } from './accountRegistry';
2
- export declare function setTerminalActiveProfile(provider: AccountProviderId, home: string | null): void;
3
- export declare function installShellHook(): void;
4
- export declare function uninstallShellHook(): void;
5
- export declare function isShellHookInstalled(): boolean;
1
+ import type { AccountProviderId } from './accountRegistry';
2
+ /**
3
+ * Write a POSIX launcher script that runs the provider CLI against a profile
4
+ * home. Unix only; on Windows use `sidekick accounts env --shell powershell`.
5
+ */
6
6
  export declare function writeLauncher(name: string, provider: AccountProviderId, profileHome: string): void;
7
7
  export declare function removeLauncher(name: string): void;
@@ -33,130 +33,20 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.setTerminalActiveProfile = setTerminalActiveProfile;
37
- exports.installShellHook = installShellHook;
38
- exports.uninstallShellHook = uninstallShellHook;
39
- exports.isShellHookInstalled = isShellHookInstalled;
40
36
  exports.writeLauncher = writeLauncher;
41
37
  exports.removeLauncher = removeLauncher;
42
38
  const fs = __importStar(require("fs"));
43
39
  const os = __importStar(require("os"));
44
40
  const path = __importStar(require("path"));
45
- const accountRegistry_1 = require("./accountRegistry");
46
- const HOOK_START = '# >>> sidekick >>>';
47
- const HOOK_END = '# <<< sidekick <<<';
41
+ const atomic_1 = require("./writers/atomic");
48
42
  const LAUNCHER_MARKER = '# sidekick-launcher v1';
49
43
  const LAUNCHER_NAME_PATTERN = /^[a-zA-Z0-9_-]+$/;
50
- function providerPointerName(provider) {
51
- return provider === 'claude-code' ? 'claude' : 'codex';
52
- }
53
44
  function providerEnvVar(provider) {
54
45
  return provider === 'claude-code' ? 'CLAUDE_CONFIG_DIR' : 'CODEX_HOME';
55
46
  }
56
47
  function providerBinary(provider) {
57
48
  return provider === 'claude-code' ? 'claude' : 'codex';
58
49
  }
59
- function getActiveProfilesDir() {
60
- return path.join((0, accountRegistry_1.getAccountsDir)(), 'active');
61
- }
62
- function getActiveProfilePath(provider) {
63
- return path.join(getActiveProfilesDir(), `${providerPointerName(provider)}.profile`);
64
- }
65
- function atomicWriteFile(filePath, content, mode = 0o600) {
66
- fs.mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o700 });
67
- const tmp = `${filePath}.tmp`;
68
- try {
69
- fs.writeFileSync(tmp, content, { encoding: 'utf8', mode });
70
- fs.renameSync(tmp, filePath);
71
- }
72
- catch (err) {
73
- try {
74
- fs.rmSync(tmp, { force: true });
75
- }
76
- catch {
77
- /* nothing to clean up */
78
- }
79
- throw err;
80
- }
81
- }
82
- function setTerminalActiveProfile(provider, home) {
83
- const pointerPath = getActiveProfilePath(provider);
84
- if (home === null) {
85
- fs.rmSync(pointerPath, { force: true });
86
- return;
87
- }
88
- atomicWriteFile(pointerPath, `${home}\n`);
89
- }
90
- function shellQuote(value) {
91
- return `'${value.replace(/'/g, `'\\''`)}'`;
92
- }
93
- function buildShellHookBlock() {
94
- const claudePointer = getActiveProfilePath('claude-code');
95
- const codexPointer = getActiveProfilePath('codex');
96
- return [
97
- HOOK_START,
98
- 'sidekick_sync() {',
99
- ` if [ -r ${shellQuote(claudePointer)} ]; then export CLAUDE_CONFIG_DIR="$(cat ${shellQuote(claudePointer)})"; else unset CLAUDE_CONFIG_DIR; fi`,
100
- ` if [ -r ${shellQuote(codexPointer)} ]; then export CODEX_HOME="$(cat ${shellQuote(codexPointer)})"; else unset CODEX_HOME; fi`,
101
- '}',
102
- 'sidekick_sync >/dev/null 2>&1',
103
- HOOK_END,
104
- '',
105
- ].join('\n');
106
- }
107
- function getShellRcPaths() {
108
- const zshrc = path.join(os.homedir(), '.zshrc');
109
- const bashrc = path.join(os.homedir(), '.bashrc');
110
- const existing = [zshrc, bashrc].filter((filePath) => fs.existsSync(filePath));
111
- if (existing.length > 0)
112
- return existing;
113
- const shell = path.basename(process.env.SHELL ?? 'zsh');
114
- return shell === 'bash' ? [bashrc] : [zshrc];
115
- }
116
- function resolveShellRcTarget(filePath) {
117
- if (!fs.existsSync(filePath))
118
- return { filePath, mode: 0o600 };
119
- const realPath = fs.realpathSync(filePath);
120
- return { filePath: realPath, mode: fs.statSync(realPath).mode & 0o777 };
121
- }
122
- function stripShellHook(content) {
123
- const pattern = new RegExp(`${HOOK_START}[\\s\\S]*?${HOOK_END}\\n?`, 'g');
124
- return content.replace(pattern, '').replace(/\n{3,}/g, '\n\n');
125
- }
126
- function installHookInFile(filePath) {
127
- const target = resolveShellRcTarget(filePath);
128
- const existing = fs.existsSync(target.filePath) ? fs.readFileSync(target.filePath, 'utf8') : '';
129
- const cleaned = stripShellHook(existing).replace(/\s*$/, '');
130
- const next = cleaned ? `${cleaned}\n\n${buildShellHookBlock()}` : buildShellHookBlock();
131
- atomicWriteFile(target.filePath, next, target.mode);
132
- }
133
- function installShellHook() {
134
- for (const rcPath of getShellRcPaths()) {
135
- installHookInFile(rcPath);
136
- }
137
- }
138
- function uninstallHookInFile(filePath) {
139
- if (!fs.existsSync(filePath))
140
- return;
141
- const target = resolveShellRcTarget(filePath);
142
- atomicWriteFile(target.filePath, stripShellHook(fs.readFileSync(target.filePath, 'utf8')), target.mode);
143
- }
144
- function uninstallShellHook() {
145
- for (const rcPath of getShellRcPaths()) {
146
- uninstallHookInFile(rcPath);
147
- }
148
- }
149
- function isShellHookInstalled() {
150
- return getShellRcPaths().some((rcPath) => {
151
- try {
152
- const content = fs.readFileSync(rcPath, 'utf8');
153
- return content.includes(HOOK_START) && content.includes(HOOK_END);
154
- }
155
- catch {
156
- return false;
157
- }
158
- });
159
- }
160
50
  function getLauncherDir() {
161
51
  return path.join(os.homedir(), '.local', 'bin');
162
52
  }
@@ -189,6 +79,10 @@ function assertNoLauncherCollision(name, targetPath) {
189
79
  }
190
80
  }
191
81
  }
82
+ /**
83
+ * Write a POSIX launcher script that runs the provider CLI against a profile
84
+ * home. Unix only; on Windows use `sidekick accounts env --shell powershell`.
85
+ */
192
86
  function writeLauncher(name, provider, profileHome) {
193
87
  assertValidLauncherName(name);
194
88
  const launcherPath = getLauncherPath(name);
@@ -199,10 +93,11 @@ function writeLauncher(name, provider, profileHome) {
199
93
  '#!/bin/sh',
200
94
  LAUNCHER_MARKER,
201
95
  `export ${envVar}=${JSON.stringify(profileHome)}`,
96
+ ...(provider === 'claude-code' ? ['unset CLAUDE_SECURESTORAGE_CONFIG_DIR'] : []),
202
97
  `exec ${binary} "$@"`,
203
98
  '',
204
99
  ].join('\n');
205
- atomicWriteFile(launcherPath, script, 0o755);
100
+ (0, atomic_1.atomicWriteFileSync)(launcherPath, script, 0o755);
206
101
  }
207
102
  function removeLauncher(name) {
208
103
  assertValidLauncherName(name);
@@ -1,3 +1,5 @@
1
+ export declare function renameSyncWithRetry(from: string, to: string): void;
2
+ export declare function renameWithRetry(from: string, to: string): Promise<void>;
1
3
  export declare function atomicWriteJson(filePath: string, value: unknown): Promise<void>;
2
4
  export declare function atomicWriteJsonSync(filePath: string, value: unknown): void;
3
5
  /** Raw-string sibling of {@link atomicWriteJsonSync} for non-JSON payloads. */
@@ -33,6 +33,8 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.renameSyncWithRetry = renameSyncWithRetry;
37
+ exports.renameWithRetry = renameWithRetry;
36
38
  exports.atomicWriteJson = atomicWriteJson;
37
39
  exports.atomicWriteJsonSync = atomicWriteJsonSync;
38
40
  exports.atomicWriteFileSync = atomicWriteFileSync;
@@ -71,6 +73,51 @@ const LOCK_ABANDONED_MS = 120_000;
71
73
  const SYNC_LOCK_MAX_WAIT_MS = 15_000;
72
74
  const LOCK_WAIT_INTERVAL_MS = 15;
73
75
  const lockSleepView = new Int32Array(new SharedArrayBuffer(4));
76
+ /**
77
+ * Windows refuses to replace a file another process holds open (antivirus
78
+ * scanners, an editor extension re-reading `auth.json`) with EPERM/EBUSY/EACCES
79
+ * even though the rename would succeed a few milliseconds later. Retry with a
80
+ * short back-off on that platform only; other platforms surface the error.
81
+ */
82
+ const WIN32_RENAME_RETRY_CODES = new Set(['EPERM', 'EBUSY', 'EACCES']);
83
+ const WIN32_RENAME_MAX_ATTEMPTS = 10;
84
+ function renameRetryDelayMs(attempt) {
85
+ return Math.min(320, 20 * 2 ** attempt);
86
+ }
87
+ function shouldRetryRename(error, attempt) {
88
+ if (process.platform !== 'win32')
89
+ return false;
90
+ if (attempt >= WIN32_RENAME_MAX_ATTEMPTS - 1)
91
+ return false;
92
+ const code = error.code ?? '';
93
+ return WIN32_RENAME_RETRY_CODES.has(code);
94
+ }
95
+ function renameSyncWithRetry(from, to) {
96
+ for (let attempt = 0;; attempt++) {
97
+ try {
98
+ fs.renameSync(from, to);
99
+ return;
100
+ }
101
+ catch (error) {
102
+ if (!shouldRetryRename(error, attempt))
103
+ throw error;
104
+ Atomics.wait(lockSleepView, 0, 0, renameRetryDelayMs(attempt));
105
+ }
106
+ }
107
+ }
108
+ async function renameWithRetry(from, to) {
109
+ for (let attempt = 0;; attempt++) {
110
+ try {
111
+ await fs.promises.rename(from, to);
112
+ return;
113
+ }
114
+ catch (error) {
115
+ if (!shouldRetryRename(error, attempt))
116
+ throw error;
117
+ await new Promise((resolve) => setTimeout(resolve, renameRetryDelayMs(attempt)));
118
+ }
119
+ }
120
+ }
74
121
  async function syncDirectory(directory) {
75
122
  let handle;
76
123
  try {
@@ -112,7 +159,7 @@ async function atomicWriteJson(filePath, value) {
112
159
  await handle.sync();
113
160
  await handle.close();
114
161
  handle = undefined;
115
- await fs.promises.rename(tempPath, filePath);
162
+ await renameWithRetry(tempPath, filePath);
116
163
  await syncDirectory(path.dirname(filePath));
117
164
  }
118
165
  catch (error) {
@@ -135,7 +182,7 @@ function atomicWriteFileSync(filePath, content, mode = 0o600) {
135
182
  fs.fsyncSync(descriptor);
136
183
  fs.closeSync(descriptor);
137
184
  descriptor = undefined;
138
- fs.renameSync(tempPath, filePath);
185
+ renameSyncWithRetry(tempPath, filePath);
139
186
  syncDirectorySync(path.dirname(filePath));
140
187
  }
141
188
  catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sidekick-shared",
3
- "version": "0.26.4",
3
+ "version": "0.26.6",
4
4
  "description": "Shared data access layer for Sidekick — readers, types, providers, credentials, quota",
5
5
  "keywords": [
6
6
  "claude-code",