@contextq/cli 0.1.0 → 0.3.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.
- package/README.md +60 -5
- package/dist/index.js +105 -393
- package/package.json +9 -5
- package/dist/desktop-guide.d.ts +0 -8
- package/dist/desktop-guide.js +0 -40
- package/dist/desktop-guide.js.map +0 -1
- package/dist/detect.d.ts +0 -42
- package/dist/detect.js +0 -70
- package/dist/detect.js.map +0 -1
- package/dist/index.d.ts +0 -17
- package/dist/index.js.map +0 -1
- package/dist/mcp-config.d.ts +0 -17
- package/dist/mcp-config.js +0 -79
- package/dist/mcp-config.js.map +0 -1
- package/dist/platform-cred.d.ts +0 -105
- package/dist/platform-cred.js +0 -210
- package/dist/platform-cred.js.map +0 -1
- package/dist/prompt.d.ts +0 -8
- package/dist/prompt.js +0 -53
- package/dist/prompt.js.map +0 -1
- package/dist/session-hook.d.ts +0 -95
- package/dist/session-hook.js +0 -217
- package/dist/session-hook.js.map +0 -1
- package/dist/skills-sync.d.ts +0 -62
- package/dist/skills-sync.js +0 -129
- package/dist/skills-sync.js.map +0 -1
- package/dist/slug.d.ts +0 -48
- package/dist/slug.js +0 -62
- package/dist/slug.js.map +0 -1
- package/dist/tasks-init.d.ts +0 -14
- package/dist/tasks-init.js +0 -51
- package/dist/tasks-init.js.map +0 -1
package/dist/platform-cred.js
DELETED
|
@@ -1,210 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* platform-cred.ts — read/write the contextq platform credential file.
|
|
3
|
-
*
|
|
4
|
-
* Same path + mode as the existing T144 web installer (src/routes/installer.ts)
|
|
5
|
-
* and the same path the contextq skill's bash scripts (contextq-api.sh,
|
|
6
|
-
* install-hook.sh, contextq-boot.sh) and Bun cred-resolve.ts already read:
|
|
7
|
-
*
|
|
8
|
-
* macOS: ~/Library/Application Support/contextq/credentials
|
|
9
|
-
* Windows: %APPDATA%/contextq/credentials
|
|
10
|
-
* Linux: $XDG_CONFIG_HOME/contextq/credentials (default ~/.config)
|
|
11
|
-
*
|
|
12
|
-
* The existing readers disagree on which key names they check: install.sh
|
|
13
|
-
* (T144) writes CTQ_API_KEY/CTQ_API_URL (read by cred-resolve.ts's Bun path),
|
|
14
|
-
* while every bash script (contextq-api.sh, install-hook.sh, contextq-boot.sh)
|
|
15
|
-
* only ever reads CONTEXT_API_KEY/CONTEXT_API_URL from this same file. Rather
|
|
16
|
-
* than picking a side, this installer writes BOTH pairs so the credential it
|
|
17
|
-
* writes is visible to every existing reader regardless of that pre-existing
|
|
18
|
-
* inconsistency.
|
|
19
|
-
*
|
|
20
|
-
* Never logs or prints the key value.
|
|
21
|
-
*
|
|
22
|
-
* T676: writeCreds() itself remains an unconditional overwrite -- callers
|
|
23
|
-
* decide when that's appropriate. The safety layer lives in the helpers
|
|
24
|
-
* below (credsFileExists/credsEqual/backupCredsFile), which index.ts's
|
|
25
|
-
* runInit() composes into: no write on an ambient env var alone when a
|
|
26
|
-
* credential file already exists, a timestamped backup before any write
|
|
27
|
-
* that would actually change an existing file, and a no-op when the new
|
|
28
|
-
* value is identical to what's already on disk.
|
|
29
|
-
*/
|
|
30
|
-
import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
31
|
-
import { homedir } from 'node:os';
|
|
32
|
-
import { join } from 'node:path';
|
|
33
|
-
/**
|
|
34
|
-
* T676: test-only override so cli/ tests can point every read/write at a
|
|
35
|
-
* throwaway temp dir instead of the real machine-wide credential path.
|
|
36
|
-
* Checked first, ahead of every OS-specific branch below. Production never
|
|
37
|
-
* sets this -- there is no code path that sets it outside a test.
|
|
38
|
-
*/
|
|
39
|
-
export function platformCredDir() {
|
|
40
|
-
const override = process.env.CTQ_CRED_DIR || process.env.CONTEXTQ_CRED_DIR;
|
|
41
|
-
if (override)
|
|
42
|
-
return override;
|
|
43
|
-
const candidates = CRED_DIR_NAMES.map(platformCredDirFor);
|
|
44
|
-
return candidates.find((dir) => existsSync(join(dir, 'credentials'))) ?? candidates[0];
|
|
45
|
-
}
|
|
46
|
-
const CRED_DIR_NAMES = ['contextq', 'ctq-brain'];
|
|
47
|
-
function platformCredDirFor(dirName) {
|
|
48
|
-
const home = homedir();
|
|
49
|
-
if (process.platform === 'darwin') {
|
|
50
|
-
return join(home, 'Library', 'Application Support', dirName);
|
|
51
|
-
}
|
|
52
|
-
if (process.platform === 'win32') {
|
|
53
|
-
const appData = process.env.APPDATA ?? join(home, 'AppData', 'Roaming');
|
|
54
|
-
return join(appData, dirName);
|
|
55
|
-
}
|
|
56
|
-
const xdg = process.env.XDG_CONFIG_HOME ?? join(home, '.config');
|
|
57
|
-
return join(xdg, dirName);
|
|
58
|
-
}
|
|
59
|
-
export function platformCredFile() {
|
|
60
|
-
return join(platformCredDir(), 'credentials');
|
|
61
|
-
}
|
|
62
|
-
/** Does a credential file already exist on disk (real or overridden dir)? */
|
|
63
|
-
export function credsFileExists() {
|
|
64
|
-
return existsSync(platformCredFile());
|
|
65
|
-
}
|
|
66
|
-
/** Same apiKey AND same apiUrl -- the "nothing would actually change" case. */
|
|
67
|
-
export function credsEqual(a, b) {
|
|
68
|
-
return a.apiKey === b.apiKey && a.apiUrl === b.apiUrl;
|
|
69
|
-
}
|
|
70
|
-
function utcTimestamp() {
|
|
71
|
-
const d = new Date();
|
|
72
|
-
const pad = (n) => String(n).padStart(2, '0');
|
|
73
|
-
return (`${d.getUTCFullYear()}${pad(d.getUTCMonth() + 1)}${pad(d.getUTCDate())}` +
|
|
74
|
-
`T${pad(d.getUTCHours())}${pad(d.getUTCMinutes())}${pad(d.getUTCSeconds())}Z`);
|
|
75
|
-
}
|
|
76
|
-
/**
|
|
77
|
-
* Copy whatever credential file is currently on disk to a timestamped
|
|
78
|
-
* sibling (mode 600, same directory) BEFORE it gets overwritten. Returns
|
|
79
|
-
* the backup path, or null when there was no existing file to back up.
|
|
80
|
-
* Never throws on missing source -- callers gate this on credsFileExists().
|
|
81
|
-
*/
|
|
82
|
-
export function backupCredsFile() {
|
|
83
|
-
const file = platformCredFile();
|
|
84
|
-
if (!existsSync(file))
|
|
85
|
-
return null;
|
|
86
|
-
const dir = platformCredDir();
|
|
87
|
-
const backupPath = join(dir, `credentials.bak-${utcTimestamp()}`);
|
|
88
|
-
copyFileSync(file, backupPath);
|
|
89
|
-
chmodSync(backupPath, 0o600);
|
|
90
|
-
return backupPath;
|
|
91
|
-
}
|
|
92
|
-
function readEnvVal(key, filePath) {
|
|
93
|
-
if (!existsSync(filePath))
|
|
94
|
-
return '';
|
|
95
|
-
try {
|
|
96
|
-
const content = readFileSync(filePath, 'utf-8');
|
|
97
|
-
for (const line of content.split('\n')) {
|
|
98
|
-
if (line.startsWith(`${key}=`))
|
|
99
|
-
return line.slice(key.length + 1).trim();
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
catch {
|
|
103
|
-
// never throw on a read error — treat as "no credential found"
|
|
104
|
-
}
|
|
105
|
-
return '';
|
|
106
|
-
}
|
|
107
|
-
/** Read whatever is already on disk. Both apiKey/apiUrl are '' when absent. */
|
|
108
|
-
export function readExistingCreds() {
|
|
109
|
-
const file = platformCredFile();
|
|
110
|
-
const apiKey = readEnvVal('CTQ_API_KEY', file) || readEnvVal('CONTEXT_API_KEY', file);
|
|
111
|
-
const apiUrl = readEnvVal('CTQ_API_URL', file) || readEnvVal('CONTEXT_API_URL', file);
|
|
112
|
-
return { apiKey, apiUrl };
|
|
113
|
-
}
|
|
114
|
-
/**
|
|
115
|
-
* Write the credential file at mode 0600 (dir at 0700). Overwrites whatever
|
|
116
|
-
* was there before — callers decide when that's appropriate (explicit --key,
|
|
117
|
-
* or no existing credential at all).
|
|
118
|
-
*/
|
|
119
|
-
export function writeCreds(creds) {
|
|
120
|
-
const dir = platformCredDir();
|
|
121
|
-
const file = platformCredFile();
|
|
122
|
-
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
123
|
-
chmodSync(dir, 0o700);
|
|
124
|
-
const body = `CTQ_API_KEY=${creds.apiKey}\n` +
|
|
125
|
-
`CTQ_API_URL=${creds.apiUrl}\n` +
|
|
126
|
-
`CONTEXT_API_KEY=${creds.apiKey}\n` +
|
|
127
|
-
`CONTEXT_API_URL=${creds.apiUrl}\n`;
|
|
128
|
-
writeFileSync(file, body, { mode: 0o600 });
|
|
129
|
-
chmodSync(file, 0o600);
|
|
130
|
-
return file;
|
|
131
|
-
}
|
|
132
|
-
export const KEY_RE = /^sk_live_[0-9a-f]{40}$/;
|
|
133
|
-
const LABEL_CREATED = '[create]';
|
|
134
|
-
const LABEL_ROTATED = '[rotate]';
|
|
135
|
-
const LABEL_SKIPPED = '[skip] ';
|
|
136
|
-
/**
|
|
137
|
-
* T676: the non-interactive half of `contextq init`'s credential
|
|
138
|
-
* resolution. Fixes the real incident this task is named for -- a demo run
|
|
139
|
-
* with only an AMBIENT env var set (direnv-exported CONTEXT_API_KEY from
|
|
140
|
-
* some repo's .env.local) used to be treated exactly like an explicit
|
|
141
|
-
* --key, silently overwriting a pre-existing machine-wide credential file
|
|
142
|
-
* with no backup. Branches, matching T676's required behavior 1-4:
|
|
143
|
-
*
|
|
144
|
-
* 1. no --key, ambient env only, file exists -> file untouched, used as-is,
|
|
145
|
-
* "skipped" naming the ignored env var
|
|
146
|
-
* 2. --key given, file exists with a different key or url -> back up the
|
|
147
|
-
* existing file (mode 600, timestamped) THEN write, "rotated"
|
|
148
|
-
* 3. --key given, file exists with the identical key AND url -> no write,
|
|
149
|
-
* "skipped (unchanged)"
|
|
150
|
-
* 4. no existing file at all -> unchanged prior behavior, "created"
|
|
151
|
-
* (works from either --key or an ambient env var, since there is
|
|
152
|
-
* nothing to protect)
|
|
153
|
-
*
|
|
154
|
-
* The one case this function does not resolve on its own is "no --key, no
|
|
155
|
-
* ambient env var, no existing file" -- action comes back null and the
|
|
156
|
-
* caller must prompt interactively, then call this function again with the
|
|
157
|
-
* typed value as `explicitKey` to get a consistent created/rotated/skipped
|
|
158
|
-
* decision (falls into branch 4 in practice, since there is no file yet).
|
|
159
|
-
*/
|
|
160
|
-
export function decideCredentialWrite(input) {
|
|
161
|
-
const existing = readExistingCreds();
|
|
162
|
-
const fileExists = credsFileExists();
|
|
163
|
-
const writeSafely = (newCreds) => {
|
|
164
|
-
if (!fileExists) {
|
|
165
|
-
writeCreds(newCreds);
|
|
166
|
-
return { action: 'created', backupPath: null };
|
|
167
|
-
}
|
|
168
|
-
if (credsEqual(existing, newCreds)) {
|
|
169
|
-
return { action: 'skipped', backupPath: null };
|
|
170
|
-
}
|
|
171
|
-
const backupPath = backupCredsFile();
|
|
172
|
-
writeCreds(newCreds);
|
|
173
|
-
return { action: 'rotated', backupPath };
|
|
174
|
-
};
|
|
175
|
-
if (input.explicitKey) {
|
|
176
|
-
if (!KEY_RE.test(input.explicitKey)) {
|
|
177
|
-
throw new Error('the provided API key does not look valid (expected sk_live_<40 hex chars>)');
|
|
178
|
-
}
|
|
179
|
-
const apiKey = input.explicitKey;
|
|
180
|
-
const result = writeSafely({ apiKey, apiUrl: input.apiUrl });
|
|
181
|
-
const credLine = result.action === 'skipped'
|
|
182
|
-
? `${LABEL_SKIPPED} credentials (unchanged)`
|
|
183
|
-
: result.action === 'rotated'
|
|
184
|
-
? `${LABEL_ROTATED} credentials (${platformCredFile()}; previous saved to ${result.backupPath})`
|
|
185
|
-
: `${LABEL_CREATED} credentials (${platformCredFile()}, mode 600)`;
|
|
186
|
-
return { apiKey, credLine, action: result.action, backupPath: result.backupPath };
|
|
187
|
-
}
|
|
188
|
-
if (fileExists && existing.apiKey) {
|
|
189
|
-
// Branch 1: an ambient env var, if any, is deliberately never even
|
|
190
|
-
// validated here -- it is not going to be used, so a garbage value must
|
|
191
|
-
// not throw.
|
|
192
|
-
const credLine = input.ambientVarName
|
|
193
|
-
? `${LABEL_SKIPPED} credentials (already configured at ${platformCredFile()}; env ${input.ambientVarName} ignored, pass --key to rotate)`
|
|
194
|
-
: `${LABEL_SKIPPED} credentials (already configured at ${platformCredFile()}; pass --key to rotate)`;
|
|
195
|
-
return { apiKey: existing.apiKey, credLine, action: 'skipped', backupPath: null };
|
|
196
|
-
}
|
|
197
|
-
if (input.ambientKey) {
|
|
198
|
-
if (!KEY_RE.test(input.ambientKey)) {
|
|
199
|
-
throw new Error('the provided API key does not look valid (expected sk_live_<40 hex chars>)');
|
|
200
|
-
}
|
|
201
|
-
const apiKey = input.ambientKey;
|
|
202
|
-
const result = writeSafely({ apiKey, apiUrl: input.apiUrl });
|
|
203
|
-
const credLine = result.action === 'rotated'
|
|
204
|
-
? `${LABEL_ROTATED} credentials (${platformCredFile()}; previous saved to ${result.backupPath})`
|
|
205
|
-
: `${LABEL_CREATED} credentials (${platformCredFile()}, mode 600)`;
|
|
206
|
-
return { apiKey, credLine, action: result.action, backupPath: result.backupPath };
|
|
207
|
-
}
|
|
208
|
-
return { apiKey: '', credLine: '', action: null, backupPath: null };
|
|
209
|
-
}
|
|
210
|
-
//# sourceMappingURL=platform-cred.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"platform-cred.js","sourceRoot":"","sources":["../src/platform-cred.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,UAAU,EAAE,SAAS,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACtG,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAOjC;;;;;GAKG;AACH,MAAM,UAAU,eAAe;IAC7B,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,YAAY,IAAI,OAAO,CAAC,GAAG,CAAC,iBAAiB,CAAC;IAC3E,IAAI,QAAQ;QAAE,OAAO,QAAQ,CAAC;IAE9B,MAAM,UAAU,GAAG,cAAc,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAC;IAC1D,OAAO,UAAU,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,GAAG,EAAE,aAAa,CAAC,CAAC,CAAC,IAAI,UAAU,CAAC,CAAC,CAAC,CAAC;AACzF,CAAC;AAED,MAAM,cAAc,GAAG,CAAC,UAAU,EAAE,WAAW,CAAU,CAAC;AAE1D,SAAS,kBAAkB,CAAC,OAAe;IACzC,MAAM,IAAI,GAAG,OAAO,EAAE,CAAC;IACvB,IAAI,OAAO,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;QAClC,OAAO,IAAI,CAAC,IAAI,EAAE,SAAS,EAAE,qBAAqB,EAAE,OAAO,CAAC,CAAC;IAC/D,CAAC;IACD,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;QACjC,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,OAAO,IAAI,IAAI,CAAC,IAAI,EAAE,SAAS,EAAE,SAAS,CAAC,CAAC;QACxE,OAAO,IAAI,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IAChC,CAAC;IACD,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,eAAe,IAAI,IAAI,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;IACjE,OAAO,IAAI,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;AAC5B,CAAC;AAED,MAAM,UAAU,gBAAgB;IAC9B,OAAO,IAAI,CAAC,eAAe,EAAE,EAAE,aAAa,CAAC,CAAC;AAChD,CAAC;AAED,6EAA6E;AAC7E,MAAM,UAAU,eAAe;IAC7B,OAAO,UAAU,CAAC,gBAAgB,EAAE,CAAC,CAAC;AACxC,CAAC;AAED,+EAA+E;AAC/E,MAAM,UAAU,UAAU,CAAC,CAAQ,EAAE,CAAQ;IAC3C,OAAO,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,MAAM,CAAC;AACxD,CAAC;AAED,SAAS,YAAY;IACnB,MAAM,CAAC,GAAG,IAAI,IAAI,EAAE,CAAC;IACrB,MAAM,GAAG,GAAG,CAAC,CAAS,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;IACtD,OAAO,CACL,GAAG,CAAC,CAAC,cAAc,EAAE,GAAG,GAAG,CAAC,CAAC,CAAC,WAAW,EAAE,GAAG,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,UAAU,EAAE,CAAC,EAAE;QACxE,IAAI,GAAG,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,aAAa,EAAE,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,aAAa,EAAE,CAAC,GAAG,CAC9E,CAAC;AACJ,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,eAAe;IAC7B,MAAM,IAAI,GAAG,gBAAgB,EAAE,CAAC;IAChC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IACnC,MAAM,GAAG,GAAG,eAAe,EAAE,CAAC;IAC9B,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,EAAE,mBAAmB,YAAY,EAAE,EAAE,CAAC,CAAC;IAClE,YAAY,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC;IAC/B,SAAS,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC;IAC7B,OAAO,UAAU,CAAC;AACpB,CAAC;AAED,SAAS,UAAU,CAAC,GAAW,EAAE,QAAgB;IAC/C,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC;QAAE,OAAO,EAAE,CAAC;IACrC,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;QAChD,KAAK,MAAM,IAAI,IAAI,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;YACvC,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,GAAG,GAAG,CAAC;gBAAE,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QAC3E,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,+DAA+D;IACjE,CAAC;IACD,OAAO,EAAE,CAAC;AACZ,CAAC;AAED,+EAA+E;AAC/E,MAAM,UAAU,iBAAiB;IAC/B,MAAM,IAAI,GAAG,gBAAgB,EAAE,CAAC;IAChC,MAAM,MAAM,GAAG,UAAU,CAAC,aAAa,EAAE,IAAI,CAAC,IAAI,UAAU,CAAC,iBAAiB,EAAE,IAAI,CAAC,CAAC;IACtF,MAAM,MAAM,GAAG,UAAU,CAAC,aAAa,EAAE,IAAI,CAAC,IAAI,UAAU,CAAC,iBAAiB,EAAE,IAAI,CAAC,CAAC;IACtF,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;AAC5B,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,UAAU,CAAC,KAAY;IACrC,MAAM,GAAG,GAAG,eAAe,EAAE,CAAC;IAC9B,MAAM,IAAI,GAAG,gBAAgB,EAAE,CAAC;IAChC,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IACjD,SAAS,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IAEtB,MAAM,IAAI,GACR,eAAe,KAAK,CAAC,MAAM,IAAI;QAC/B,eAAe,KAAK,CAAC,MAAM,IAAI;QAC/B,mBAAmB,KAAK,CAAC,MAAM,IAAI;QACnC,mBAAmB,KAAK,CAAC,MAAM,IAAI,CAAC;IAEtC,aAAa,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IAC3C,SAAS,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IACvB,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,CAAC,MAAM,MAAM,GAAG,wBAAwB,CAAC;AAE/C,MAAM,aAAa,GAAG,UAAU,CAAC;AACjC,MAAM,aAAa,GAAG,UAAU,CAAC;AACjC,MAAM,aAAa,GAAG,SAAS,CAAC;AAuBhC;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,UAAU,qBAAqB,CAAC,KAA8B;IAClE,MAAM,QAAQ,GAAG,iBAAiB,EAAE,CAAC;IACrC,MAAM,UAAU,GAAG,eAAe,EAAE,CAAC;IAErC,MAAM,WAAW,GAAG,CAClB,QAAe,EAC2D,EAAE;QAC5E,IAAI,CAAC,UAAU,EAAE,CAAC;YAChB,UAAU,CAAC,QAAQ,CAAC,CAAC;YACrB,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC;QACjD,CAAC;QACD,IAAI,UAAU,CAAC,QAAQ,EAAE,QAAQ,CAAC,EAAE,CAAC;YACnC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC;QACjD,CAAC;QACD,MAAM,UAAU,GAAG,eAAe,EAAE,CAAC;QACrC,UAAU,CAAC,QAAQ,CAAC,CAAC;QACrB,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,UAAU,EAAE,CAAC;IAC3C,CAAC,CAAC;IAEF,IAAI,KAAK,CAAC,WAAW,EAAE,CAAC;QACtB,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,EAAE,CAAC;YACpC,MAAM,IAAI,KAAK,CAAC,4EAA4E,CAAC,CAAC;QAChG,CAAC;QACD,MAAM,MAAM,GAAG,KAAK,CAAC,WAAW,CAAC;QACjC,MAAM,MAAM,GAAG,WAAW,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC;QAC7D,MAAM,QAAQ,GACZ,MAAM,CAAC,MAAM,KAAK,SAAS;YACzB,CAAC,CAAC,GAAG,aAAa,0BAA0B;YAC5C,CAAC,CAAC,MAAM,CAAC,MAAM,KAAK,SAAS;gBAC3B,CAAC,CAAC,GAAG,aAAa,iBAAiB,gBAAgB,EAAE,uBAAuB,MAAM,CAAC,UAAU,GAAG;gBAChG,CAAC,CAAC,GAAG,aAAa,iBAAiB,gBAAgB,EAAE,aAAa,CAAC;QACzE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,EAAE,MAAM,CAAC,UAAU,EAAE,CAAC;IACpF,CAAC;IAED,IAAI,UAAU,IAAI,QAAQ,CAAC,MAAM,EAAE,CAAC;QAClC,mEAAmE;QACnE,wEAAwE;QACxE,aAAa;QACb,MAAM,QAAQ,GAAG,KAAK,CAAC,cAAc;YACnC,CAAC,CAAC,GAAG,aAAa,uCAAuC,gBAAgB,EAAE,SAAS,KAAK,CAAC,cAAc,iCAAiC;YACzI,CAAC,CAAC,GAAG,aAAa,uCAAuC,gBAAgB,EAAE,yBAAyB,CAAC;QACvG,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC;IACpF,CAAC;IAED,IAAI,KAAK,CAAC,UAAU,EAAE,CAAC;QACrB,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,EAAE,CAAC;YACnC,MAAM,IAAI,KAAK,CAAC,4EAA4E,CAAC,CAAC;QAChG,CAAC;QACD,MAAM,MAAM,GAAG,KAAK,CAAC,UAAU,CAAC;QAChC,MAAM,MAAM,GAAG,WAAW,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC;QAC7D,MAAM,QAAQ,GACZ,MAAM,CAAC,MAAM,KAAK,SAAS;YACzB,CAAC,CAAC,GAAG,aAAa,iBAAiB,gBAAgB,EAAE,uBAAuB,MAAM,CAAC,UAAU,GAAG;YAChG,CAAC,CAAC,GAAG,aAAa,iBAAiB,gBAAgB,EAAE,aAAa,CAAC;QACvE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,EAAE,MAAM,CAAC,UAAU,EAAE,CAAC;IACpF,CAAC;IAED,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC;AACtE,CAAC"}
|
package/dist/prompt.d.ts
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* prompt.ts — masked stdin prompt for the API key, no extra dependency.
|
|
3
|
-
*
|
|
4
|
-
* Returns '' immediately when stdin is not a TTY (non-interactive/CI run) --
|
|
5
|
-
* callers must treat that as "no key available" and fail with a clear
|
|
6
|
-
* message rather than hang waiting for input that will never arrive.
|
|
7
|
-
*/
|
|
8
|
-
export declare function promptHidden(question: string): Promise<string>;
|
package/dist/prompt.js
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* prompt.ts — masked stdin prompt for the API key, no extra dependency.
|
|
3
|
-
*
|
|
4
|
-
* Returns '' immediately when stdin is not a TTY (non-interactive/CI run) --
|
|
5
|
-
* callers must treat that as "no key available" and fail with a clear
|
|
6
|
-
* message rather than hang waiting for input that will never arrive.
|
|
7
|
-
*/
|
|
8
|
-
const CTRL_C = String.fromCharCode(3);
|
|
9
|
-
const CTRL_D = String.fromCharCode(4);
|
|
10
|
-
const DEL = String.fromCharCode(127);
|
|
11
|
-
export async function promptHidden(question) {
|
|
12
|
-
if (!process.stdin.isTTY)
|
|
13
|
-
return '';
|
|
14
|
-
return new Promise((resolve) => {
|
|
15
|
-
const stdin = process.stdin;
|
|
16
|
-
const stdout = process.stdout;
|
|
17
|
-
stdout.write(question);
|
|
18
|
-
let input = '';
|
|
19
|
-
const onData = (chunk) => {
|
|
20
|
-
const char = chunk.toString('utf8');
|
|
21
|
-
switch (char) {
|
|
22
|
-
case '\n':
|
|
23
|
-
case '\r':
|
|
24
|
-
case CTRL_D:
|
|
25
|
-
stdin.setRawMode?.(false);
|
|
26
|
-
stdin.pause();
|
|
27
|
-
stdin.removeListener('data', onData);
|
|
28
|
-
stdout.write('\n');
|
|
29
|
-
resolve(input.trim());
|
|
30
|
-
break;
|
|
31
|
-
case CTRL_C:
|
|
32
|
-
stdout.write('\n');
|
|
33
|
-
process.exit(130);
|
|
34
|
-
break;
|
|
35
|
-
case DEL:
|
|
36
|
-
case '\b':
|
|
37
|
-
if (input.length > 0) {
|
|
38
|
-
input = input.slice(0, -1);
|
|
39
|
-
stdout.write('\b \b');
|
|
40
|
-
}
|
|
41
|
-
break;
|
|
42
|
-
default:
|
|
43
|
-
input += char;
|
|
44
|
-
stdout.write('*');
|
|
45
|
-
}
|
|
46
|
-
};
|
|
47
|
-
stdin.setRawMode?.(true);
|
|
48
|
-
stdin.resume();
|
|
49
|
-
stdin.setEncoding('utf8');
|
|
50
|
-
stdin.on('data', onData);
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
//# sourceMappingURL=prompt.js.map
|
package/dist/prompt.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"prompt.js","sourceRoot":"","sources":["../src/prompt.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,MAAM,GAAG,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;AACtC,MAAM,MAAM,GAAG,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;AACtC,MAAM,GAAG,GAAG,MAAM,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC;AAErC,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,QAAgB;IACjD,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK;QAAE,OAAO,EAAE,CAAC;IAEpC,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QAC7B,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC;QAC5B,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;QAC9B,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;QAEvB,IAAI,KAAK,GAAG,EAAE,CAAC;QACf,MAAM,MAAM,GAAG,CAAC,KAAa,EAAQ,EAAE;YACrC,MAAM,IAAI,GAAG,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;YACpC,QAAQ,IAAI,EAAE,CAAC;gBACb,KAAK,IAAI,CAAC;gBACV,KAAK,IAAI,CAAC;gBACV,KAAK,MAAM;oBACT,KAAK,CAAC,UAAU,EAAE,CAAC,KAAK,CAAC,CAAC;oBAC1B,KAAK,CAAC,KAAK,EAAE,CAAC;oBACd,KAAK,CAAC,cAAc,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;oBACrC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;oBACnB,OAAO,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC;oBACtB,MAAM;gBACR,KAAK,MAAM;oBACT,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;oBACnB,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;oBAClB,MAAM;gBACR,KAAK,GAAG,CAAC;gBACT,KAAK,IAAI;oBACP,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;wBACrB,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;wBAC3B,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;oBACxB,CAAC;oBACD,MAAM;gBACR;oBACE,KAAK,IAAI,IAAI,CAAC;oBACd,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;YACtB,CAAC;QACH,CAAC,CAAC;QAEF,KAAK,CAAC,UAAU,EAAE,CAAC,IAAI,CAAC,CAAC;QACzB,KAAK,CAAC,MAAM,EAAE,CAAC;QACf,KAAK,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;QAC1B,KAAK,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC3B,CAAC,CAAC,CAAC;AACL,CAAC"}
|
package/dist/session-hook.d.ts
DELETED
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* session-hook.ts — idempotent SessionStart boot-hook merge, for BOTH
|
|
3
|
-
* Claude Code and Codex CLI (T665).
|
|
4
|
-
*
|
|
5
|
-
* Targets <root>/.claude/settings.local.json for Claude Code, NOT
|
|
6
|
-
* settings.json -- mirroring skills/contextq/scripts/install-hook.sh's own
|
|
7
|
-
* explicit rule: the baked hook command embeds an absolute $HOME/... path,
|
|
8
|
-
* which is per-machine and MUST stay in a gitignored file, never a
|
|
9
|
-
* shared/global settings file (see install-hook.sh header comment).
|
|
10
|
-
*
|
|
11
|
-
* Codex's own convention treats project-level `.codex/config.toml` /
|
|
12
|
-
* `.codex/hooks.json` as "trusted-repo settings" meant to be COMMITTED and
|
|
13
|
-
* shared across a team (verified against developers.openai.com/codex docs,
|
|
14
|
-
* 2026-08-20: "Project `.codex/config.toml` -> trusted-repo Codex settings
|
|
15
|
-
* such as sandbox, MCP, hooks, model, or reasoning defaults") -- there is no
|
|
16
|
-
* Codex-native equivalent of Claude's settings.json/settings.local.json
|
|
17
|
-
* split. Since the command we write embeds the SAME per-machine absolute
|
|
18
|
-
* path either way, wireCodexSessionHook() deliberately breaks from that
|
|
19
|
-
* convention and gitignores `.codex/hooks.json` itself (see
|
|
20
|
-
* ensureGitignored below) rather than let a per-machine path get committed
|
|
21
|
-
* and silently break for every teammate whose contextq skill lives
|
|
22
|
-
* somewhere else (or isn't installed at all).
|
|
23
|
-
*
|
|
24
|
-
* This installer POINTS AT the contextq skill's boot script when it is
|
|
25
|
-
* already installed globally at ~/.claude/skills/contextq -- it never
|
|
26
|
-
* copies/vendors the skill tree into the target project (out of this
|
|
27
|
-
* installer's declared write-scope: .mcp.json, .claude/settings*.json,
|
|
28
|
-
* .codex/hooks.json, .contextq/, and the credential file only).
|
|
29
|
-
*
|
|
30
|
-
* ONE shared code path for both harnesses (T665 requirement), in two
|
|
31
|
-
* layers:
|
|
32
|
-
* - buildBootCommand() is the single command-string builder both writers
|
|
33
|
-
* call -- same env vars, same shellQuote()-based escaping, same target
|
|
34
|
-
* script.
|
|
35
|
-
* - mergeSessionStartHook() is the single read-merge-idempotent-write
|
|
36
|
-
* implementation both writers call. Claude's `.claude/settings.local.json`
|
|
37
|
-
* and Codex's `.codex/hooks.json` use the IDENTICAL JSON shape for a
|
|
38
|
-
* SessionStart entry -- { hooks: { SessionStart: [ { hooks: [ { type:
|
|
39
|
-
* 'command', command } ] } ] } } (verified against Codex's `hooks.json`
|
|
40
|
-
* schema at developers.openai.com/codex/hooks, 2026-08-20) -- so there is
|
|
41
|
-
* exactly one implementation of "does this file already have our hook,
|
|
42
|
-
* and if not, append it", parameterized by which file path to merge
|
|
43
|
-
* into. The two exported writers (wireSessionHook, wireCodexSessionHook)
|
|
44
|
-
* are thin wrappers supplying only the path + an optional top-level
|
|
45
|
-
* default field Codex's schema additionally supports ("description").
|
|
46
|
-
*/
|
|
47
|
-
export interface HookWireResult {
|
|
48
|
-
path: string;
|
|
49
|
-
action: 'wired' | 'skipped';
|
|
50
|
-
detail: string;
|
|
51
|
-
}
|
|
52
|
-
/**
|
|
53
|
-
* T656: POSIX single-quote wrap, safe for any input including shell
|
|
54
|
-
* metacharacters, unbalanced quotes, backticks, `$()`, `;`, etc. This is the
|
|
55
|
-
* defense-in-depth layer: slug.ts's validateSlug()/sanitizeSlugFallback()
|
|
56
|
-
* already constrain --agent/--workspace to a safe charset before they reach
|
|
57
|
-
* here, but this function must make the written command safe on its own,
|
|
58
|
-
* because a future caller of wireSessionHook()/wireCodexSessionHook() might
|
|
59
|
-
* bypass that validation (it takes plain strings, not a branded/validated
|
|
60
|
-
* type) -- every value substituted into the command string, including
|
|
61
|
-
* bootScript itself, goes through this.
|
|
62
|
-
*
|
|
63
|
-
* Standard technique: wrap in single quotes (nothing is interpolated inside
|
|
64
|
-
* single quotes in POSIX sh/bash), and for each embedded single quote,
|
|
65
|
-
* close the quote, emit an escaped literal quote, then reopen: ' -> '\''.
|
|
66
|
-
*/
|
|
67
|
-
export declare function shellQuote(value: string): string;
|
|
68
|
-
export declare function ctqBrainBootScript(): string;
|
|
69
|
-
/**
|
|
70
|
-
* buildBootCommand — the ONE harness-agnostic command builder (T665). Both
|
|
71
|
-
* wireSessionHook (Claude) and wireCodexSessionHook (Codex) call this to
|
|
72
|
-
* produce the exact same shell command string. Codex's SessionStart stdin
|
|
73
|
-
* contract and its `hookSpecificOutput.additionalContext` output shape are
|
|
74
|
-
* verified (2026-08-20, developers.openai.com/codex/hooks) to match Claude's
|
|
75
|
-
* field-for-field (session_id, transcript_path, cwd, hook_event_name on
|
|
76
|
-
* stdin; the same hookSpecificOutput.additionalContext JSON, or plain text,
|
|
77
|
-
* on stdout) -- so contextq-boot.sh's existing --hook output needs no
|
|
78
|
-
* per-harness branching, and this builder needs none either.
|
|
79
|
-
*/
|
|
80
|
-
export declare function buildBootCommand(agentSlug: string, workspace: string, bootScript: string): string;
|
|
81
|
-
/**
|
|
82
|
-
* `bootScriptOverride` exists purely for tests: production callers (index.ts)
|
|
83
|
-
* always call this with 3 args and get ctqBrainBootScript(), which points at
|
|
84
|
-
* a real, per-machine, possibly-absent path under $HOME -- tests need to
|
|
85
|
-
* exercise the "wired" branch deterministically without depending on the
|
|
86
|
-
* host actually having contextq installed.
|
|
87
|
-
*/
|
|
88
|
-
export declare function wireSessionHook(root: string, agentSlug: string, workspace: string, bootScriptOverride?: string): HookWireResult;
|
|
89
|
-
/**
|
|
90
|
-
* wireCodexSessionHook — the Codex writer (T665). Same command, same merge
|
|
91
|
-
* core as wireSessionHook above; only the target path, the top-level
|
|
92
|
-
* "description" default, and the gitignore step differ. `bootScriptOverride`
|
|
93
|
-
* exists for the same test-determinism reason as wireSessionHook's.
|
|
94
|
-
*/
|
|
95
|
-
export declare function wireCodexSessionHook(root: string, agentSlug: string, workspace: string, bootScriptOverride?: string): HookWireResult;
|
package/dist/session-hook.js
DELETED
|
@@ -1,217 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* session-hook.ts — idempotent SessionStart boot-hook merge, for BOTH
|
|
3
|
-
* Claude Code and Codex CLI (T665).
|
|
4
|
-
*
|
|
5
|
-
* Targets <root>/.claude/settings.local.json for Claude Code, NOT
|
|
6
|
-
* settings.json -- mirroring skills/contextq/scripts/install-hook.sh's own
|
|
7
|
-
* explicit rule: the baked hook command embeds an absolute $HOME/... path,
|
|
8
|
-
* which is per-machine and MUST stay in a gitignored file, never a
|
|
9
|
-
* shared/global settings file (see install-hook.sh header comment).
|
|
10
|
-
*
|
|
11
|
-
* Codex's own convention treats project-level `.codex/config.toml` /
|
|
12
|
-
* `.codex/hooks.json` as "trusted-repo settings" meant to be COMMITTED and
|
|
13
|
-
* shared across a team (verified against developers.openai.com/codex docs,
|
|
14
|
-
* 2026-08-20: "Project `.codex/config.toml` -> trusted-repo Codex settings
|
|
15
|
-
* such as sandbox, MCP, hooks, model, or reasoning defaults") -- there is no
|
|
16
|
-
* Codex-native equivalent of Claude's settings.json/settings.local.json
|
|
17
|
-
* split. Since the command we write embeds the SAME per-machine absolute
|
|
18
|
-
* path either way, wireCodexSessionHook() deliberately breaks from that
|
|
19
|
-
* convention and gitignores `.codex/hooks.json` itself (see
|
|
20
|
-
* ensureGitignored below) rather than let a per-machine path get committed
|
|
21
|
-
* and silently break for every teammate whose contextq skill lives
|
|
22
|
-
* somewhere else (or isn't installed at all).
|
|
23
|
-
*
|
|
24
|
-
* This installer POINTS AT the contextq skill's boot script when it is
|
|
25
|
-
* already installed globally at ~/.claude/skills/contextq -- it never
|
|
26
|
-
* copies/vendors the skill tree into the target project (out of this
|
|
27
|
-
* installer's declared write-scope: .mcp.json, .claude/settings*.json,
|
|
28
|
-
* .codex/hooks.json, .contextq/, and the credential file only).
|
|
29
|
-
*
|
|
30
|
-
* ONE shared code path for both harnesses (T665 requirement), in two
|
|
31
|
-
* layers:
|
|
32
|
-
* - buildBootCommand() is the single command-string builder both writers
|
|
33
|
-
* call -- same env vars, same shellQuote()-based escaping, same target
|
|
34
|
-
* script.
|
|
35
|
-
* - mergeSessionStartHook() is the single read-merge-idempotent-write
|
|
36
|
-
* implementation both writers call. Claude's `.claude/settings.local.json`
|
|
37
|
-
* and Codex's `.codex/hooks.json` use the IDENTICAL JSON shape for a
|
|
38
|
-
* SessionStart entry -- { hooks: { SessionStart: [ { hooks: [ { type:
|
|
39
|
-
* 'command', command } ] } ] } } (verified against Codex's `hooks.json`
|
|
40
|
-
* schema at developers.openai.com/codex/hooks, 2026-08-20) -- so there is
|
|
41
|
-
* exactly one implementation of "does this file already have our hook,
|
|
42
|
-
* and if not, append it", parameterized by which file path to merge
|
|
43
|
-
* into. The two exported writers (wireSessionHook, wireCodexSessionHook)
|
|
44
|
-
* are thin wrappers supplying only the path + an optional top-level
|
|
45
|
-
* default field Codex's schema additionally supports ("description").
|
|
46
|
-
*/
|
|
47
|
-
import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
48
|
-
import { homedir } from 'node:os';
|
|
49
|
-
import { join } from 'node:path';
|
|
50
|
-
const MARKER = /contextq-boot\.sh|contextq.*boot/;
|
|
51
|
-
/**
|
|
52
|
-
* T656: POSIX single-quote wrap, safe for any input including shell
|
|
53
|
-
* metacharacters, unbalanced quotes, backticks, `$()`, `;`, etc. This is the
|
|
54
|
-
* defense-in-depth layer: slug.ts's validateSlug()/sanitizeSlugFallback()
|
|
55
|
-
* already constrain --agent/--workspace to a safe charset before they reach
|
|
56
|
-
* here, but this function must make the written command safe on its own,
|
|
57
|
-
* because a future caller of wireSessionHook()/wireCodexSessionHook() might
|
|
58
|
-
* bypass that validation (it takes plain strings, not a branded/validated
|
|
59
|
-
* type) -- every value substituted into the command string, including
|
|
60
|
-
* bootScript itself, goes through this.
|
|
61
|
-
*
|
|
62
|
-
* Standard technique: wrap in single quotes (nothing is interpolated inside
|
|
63
|
-
* single quotes in POSIX sh/bash), and for each embedded single quote,
|
|
64
|
-
* close the quote, emit an escaped literal quote, then reopen: ' -> '\''.
|
|
65
|
-
*/
|
|
66
|
-
export function shellQuote(value) {
|
|
67
|
-
return `'${value.split("'").join("'\\''")}'`;
|
|
68
|
-
}
|
|
69
|
-
export function ctqBrainBootScript() {
|
|
70
|
-
return join(homedir(), '.claude', 'skills', 'contextq', 'scripts', 'contextq-boot.sh');
|
|
71
|
-
}
|
|
72
|
-
/**
|
|
73
|
-
* buildBootCommand — the ONE harness-agnostic command builder (T665). Both
|
|
74
|
-
* wireSessionHook (Claude) and wireCodexSessionHook (Codex) call this to
|
|
75
|
-
* produce the exact same shell command string. Codex's SessionStart stdin
|
|
76
|
-
* contract and its `hookSpecificOutput.additionalContext` output shape are
|
|
77
|
-
* verified (2026-08-20, developers.openai.com/codex/hooks) to match Claude's
|
|
78
|
-
* field-for-field (session_id, transcript_path, cwd, hook_event_name on
|
|
79
|
-
* stdin; the same hookSpecificOutput.additionalContext JSON, or plain text,
|
|
80
|
-
* on stdout) -- so contextq-boot.sh's existing --hook output needs no
|
|
81
|
-
* per-harness branching, and this builder needs none either.
|
|
82
|
-
*/
|
|
83
|
-
export function buildBootCommand(agentSlug, workspace, bootScript) {
|
|
84
|
-
return (`CONTEXTQ_AGENT_SLUG=${shellQuote(agentSlug)} CONTEXTQ_WORKSPACE=${shellQuote(workspace)} ` +
|
|
85
|
-
`CONTEXTQ_WRITE_WORKSPACE=${shellQuote(workspace)} CONTEXTQ_READ_SCOPE=own+global ` +
|
|
86
|
-
`bash ${shellQuote(bootScript)} --hook`);
|
|
87
|
-
}
|
|
88
|
-
/**
|
|
89
|
-
* mergeSessionStartHook — the ONE shared read-merge-idempotent-write core
|
|
90
|
-
* (T665). See the file header for why this is safe to share verbatim
|
|
91
|
-
* between Claude's settings.local.json and Codex's hooks.json.
|
|
92
|
-
*/
|
|
93
|
-
function mergeSessionStartHook(target, command, wiredDetail) {
|
|
94
|
-
const { path, dir, topLevelDefaults } = target;
|
|
95
|
-
const existed = existsSync(path);
|
|
96
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
97
|
-
let doc = {};
|
|
98
|
-
if (existed) {
|
|
99
|
-
const raw = readFileSync(path, 'utf-8');
|
|
100
|
-
try {
|
|
101
|
-
doc = raw.trim() === '' ? {} : JSON.parse(raw);
|
|
102
|
-
}
|
|
103
|
-
catch (err) {
|
|
104
|
-
throw new Error(`${path} exists but is not valid JSON (${err.message}) -- refusing to touch it`);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
if (topLevelDefaults) {
|
|
108
|
-
for (const [key, value] of Object.entries(topLevelDefaults)) {
|
|
109
|
-
if (doc[key] === undefined)
|
|
110
|
-
doc[key] = value;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
doc.hooks ??= {};
|
|
114
|
-
doc.hooks.SessionStart ??= [];
|
|
115
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
116
|
-
const already = doc.hooks.SessionStart.some((entry) => (entry?.hooks ?? []).some((h) => typeof h.command === 'string' && MARKER.test(h.command)));
|
|
117
|
-
if (already) {
|
|
118
|
-
return { path, action: 'skipped', detail: 'SessionStart contextq-boot hook already present' };
|
|
119
|
-
}
|
|
120
|
-
doc.hooks.SessionStart.push({ hooks: [{ type: 'command', command }] });
|
|
121
|
-
mkdirSync(dir, { recursive: true });
|
|
122
|
-
writeFileSync(path, `${JSON.stringify(doc, null, 2)}\n`);
|
|
123
|
-
return { path, action: 'wired', detail: wiredDetail };
|
|
124
|
-
}
|
|
125
|
-
/**
|
|
126
|
-
* `bootScriptOverride` exists purely for tests: production callers (index.ts)
|
|
127
|
-
* always call this with 3 args and get ctqBrainBootScript(), which points at
|
|
128
|
-
* a real, per-machine, possibly-absent path under $HOME -- tests need to
|
|
129
|
-
* exercise the "wired" branch deterministically without depending on the
|
|
130
|
-
* host actually having contextq installed.
|
|
131
|
-
*/
|
|
132
|
-
export function wireSessionHook(root, agentSlug, workspace, bootScriptOverride) {
|
|
133
|
-
const path = join(root, '.claude', 'settings.local.json');
|
|
134
|
-
const bootScript = bootScriptOverride ?? ctqBrainBootScript();
|
|
135
|
-
if (!existsSync(bootScript)) {
|
|
136
|
-
return {
|
|
137
|
-
path,
|
|
138
|
-
action: 'skipped',
|
|
139
|
-
detail: 'contextq skill not found at ~/.claude/skills/contextq -- MCP tools and the task ' +
|
|
140
|
-
'manifest are wired, but the memory boot hook needs the skill installed separately first',
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
|
-
const command = buildBootCommand(agentSlug, workspace, bootScript);
|
|
144
|
-
return mergeSessionStartHook({ path, dir: join(root, '.claude') }, command, `SessionStart boot hook wired (agent=${agentSlug}, workspace=${workspace})`);
|
|
145
|
-
}
|
|
146
|
-
/**
|
|
147
|
-
* T665: whether `<root>/.codex/config.toml` ALSO declares an inline
|
|
148
|
-
* `[hooks]`/`[[hooks.X]]` table. Codex's own docs (developers.openai.com/
|
|
149
|
-
* codex/hooks, 2026-08-20) state that when one config layer holds both a
|
|
150
|
-
* `hooks.json` file and an inline `[hooks]` table, "Codex loads both and
|
|
151
|
-
* warns. Prefer one representation per layer." -- we always write
|
|
152
|
-
* `hooks.json`, so this surfaces that collision to the installer's own
|
|
153
|
-
* output instead of leaving it as a silent Codex-side warning nobody sees.
|
|
154
|
-
*/
|
|
155
|
-
function hasInlineHooksTable(root) {
|
|
156
|
-
const configPath = join(root, '.codex', 'config.toml');
|
|
157
|
-
if (!existsSync(configPath))
|
|
158
|
-
return false;
|
|
159
|
-
const raw = readFileSync(configPath, 'utf-8');
|
|
160
|
-
return /^\s*\[{1,2}hooks(\.|\])/m.test(raw);
|
|
161
|
-
}
|
|
162
|
-
/**
|
|
163
|
-
* T665: `.codex/hooks.json` is gitignored the same way `.claude/settings.local.json`
|
|
164
|
-
* effectively is (see the file header) -- but Codex has no built-in
|
|
165
|
-
* settings.json/settings.local.json split, so THIS installer has to be the
|
|
166
|
-
* one to add the .gitignore line, not Codex itself.
|
|
167
|
-
*/
|
|
168
|
-
function ensureGitignored(root, pattern) {
|
|
169
|
-
if (!existsSync(join(root, '.git')))
|
|
170
|
-
return;
|
|
171
|
-
const gitignorePath = join(root, '.gitignore');
|
|
172
|
-
const existing = existsSync(gitignorePath) ? readFileSync(gitignorePath, 'utf-8') : '';
|
|
173
|
-
const existingLines = new Set(existing.split('\n').map((line) => line.trim()));
|
|
174
|
-
if (existingLines.has(pattern))
|
|
175
|
-
return;
|
|
176
|
-
const block = '\n# .codex/hooks.json embeds a per-machine absolute path (the contextq boot\n' +
|
|
177
|
-
'# hook command) -- gitignored the same way .claude/settings.local.json is.\n' +
|
|
178
|
-
'# Each teammate who wants it needs to run `npx contextq init` themselves.\n' +
|
|
179
|
-
`${pattern}\n`;
|
|
180
|
-
appendFileSync(gitignorePath, block);
|
|
181
|
-
}
|
|
182
|
-
/**
|
|
183
|
-
* wireCodexSessionHook — the Codex writer (T665). Same command, same merge
|
|
184
|
-
* core as wireSessionHook above; only the target path, the top-level
|
|
185
|
-
* "description" default, and the gitignore step differ. `bootScriptOverride`
|
|
186
|
-
* exists for the same test-determinism reason as wireSessionHook's.
|
|
187
|
-
*/
|
|
188
|
-
export function wireCodexSessionHook(root, agentSlug, workspace, bootScriptOverride) {
|
|
189
|
-
const path = join(root, '.codex', 'hooks.json');
|
|
190
|
-
const bootScript = bootScriptOverride ?? ctqBrainBootScript();
|
|
191
|
-
if (!existsSync(bootScript)) {
|
|
192
|
-
return {
|
|
193
|
-
path,
|
|
194
|
-
action: 'skipped',
|
|
195
|
-
detail: 'contextq skill not found at ~/.claude/skills/contextq -- the memory boot hook needs ' +
|
|
196
|
-
'the skill installed separately first',
|
|
197
|
-
};
|
|
198
|
-
}
|
|
199
|
-
const command = buildBootCommand(agentSlug, workspace, bootScript);
|
|
200
|
-
const trustNote = 'Codex requires this hook to be TRUSTED before it runs -- start `codex` in this repo and ' +
|
|
201
|
-
'approve it at the hook-trust prompt (or review/trust via `/hooks` in a session)';
|
|
202
|
-
const inlineWarning = hasInlineHooksTable(root)
|
|
203
|
-
? ` -- WARNING: .codex/config.toml also declares an inline [hooks] table; Codex loads both hooks.json and the inline table and warns on every run. Prefer one representation per layer.`
|
|
204
|
-
: '';
|
|
205
|
-
const result = mergeSessionStartHook({
|
|
206
|
-
path,
|
|
207
|
-
dir: join(root, '.codex'),
|
|
208
|
-
topLevelDefaults: {
|
|
209
|
-
description: 'ContextQ boot hook (managed by `npx contextq init` -- re-run the installer to update, do not hand-edit)',
|
|
210
|
-
},
|
|
211
|
-
}, command, `SessionStart boot hook wired (agent=${agentSlug}, workspace=${workspace}). ${trustNote}.${inlineWarning}`);
|
|
212
|
-
if (result.action === 'wired') {
|
|
213
|
-
ensureGitignored(root, '.codex/hooks.json');
|
|
214
|
-
}
|
|
215
|
-
return result;
|
|
216
|
-
}
|
|
217
|
-
//# sourceMappingURL=session-hook.js.map
|
package/dist/session-hook.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"session-hook.js","sourceRoot":"","sources":["../src/session-hook.ts"],"names":[],"mappings":"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"}
|