@chemx/create-chemx 26.9.20-228 → 26.9.20-369
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/blueprints/_package.json +17 -0
- package/blueprints/_tsconfig.json +15 -0
- package/blueprints/_vitest.config.ts +9 -0
- package/cli/generator.d.ts +3 -2
- package/cli/generator.js +59 -0
- package/cli/generator.spec.js +17 -0
- package/cli/help.js +18 -0
- package/cli/mcp/installer.spec.js +4 -6
- package/cli/patcher.js +42 -0
- package/cli/patcher.spec.js +19 -0
- package/cli/reader.js +25 -0
- package/cli/scaffold.js +27 -7
- package/cli/team/team-multi-model-telemetry.spec.js +80 -0
- package/cli/team/team-schema.js +6 -0
- package/cli/team/team-telemetry.js +61 -38
- package/cli/theme.js +1 -0
- package/cli/ui-actions-codebase.js +75 -0
- package/cli/ui-actions-studio.js +91 -0
- package/cli/ui-actions-tasks.js +87 -0
- package/cli/ui-actions.js +11 -27
- package/cli/ui-canonical-routes.spec.js +130 -0
- package/cli/ui-client-api.js +58 -20
- package/cli/ui-client-filetree.js +93 -0
- package/cli/ui-client-forum.js +85 -0
- package/cli/ui-client-script.js +52 -37
- package/cli/ui-client-studio.js +95 -0
- package/cli/ui-e2e-verification.spec.js +186 -0
- package/cli/ui-filetree.spec.js +175 -0
- package/cli/ui-forum-data.js +97 -0
- package/cli/ui-forum-topics.js +65 -0
- package/cli/ui-handlers.js +61 -65
- package/cli/ui-html.js +6 -0
- package/cli/ui-kanban.spec.js +206 -0
- package/cli/ui-server-routes.js +56 -28
- package/cli/ui-server.js +12 -6
- package/cli/ui-styles.js +80 -27
- package/cli/ui-template-agents.js +64 -0
- package/cli/ui-template-dbstudio.js +82 -0
- package/cli/ui-template-filetree.js +92 -0
- package/cli/ui-template-forum.js +86 -0
- package/cli/ui-template-kanban.js +83 -0
- package/cli/ui-template-thread.js +51 -0
- package/cli/ui-template-timeline.js +35 -0
- package/cli/ui-template-workbench.js +55 -0
- package/cli/ui-template.js +14 -34
- package/cli/ui-vbulletin.spec.js +189 -0
- package/cli/ui-workbench-dbstudio.spec.js +172 -0
- package/cli/verify.js +107 -16
- package/package.json +1 -1
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "chemical-x-app",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "vitest run",
|
|
8
|
+
"typecheck": "tsc --noEmit",
|
|
9
|
+
"audit": "chemx audit",
|
|
10
|
+
"verify": "chemx verify"
|
|
11
|
+
},
|
|
12
|
+
"devDependencies": {
|
|
13
|
+
"create-chemx": "latest",
|
|
14
|
+
"typescript": "^5.4.0",
|
|
15
|
+
"vitest": "^1.6.0"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"forceConsistentCasingInFileNames": true,
|
|
10
|
+
"jsx": "react-jsx",
|
|
11
|
+
"lib": ["DOM", "DOM.Iterable", "ES2022"],
|
|
12
|
+
"types": ["vitest/globals"]
|
|
13
|
+
},
|
|
14
|
+
"include": ["**/*.ts", "**/*.tsx", "**/*.vue", "**/*.d.ts"]
|
|
15
|
+
}
|
package/cli/generator.d.ts
CHANGED
|
@@ -19,5 +19,6 @@ export interface CapsuleResult {
|
|
|
19
19
|
|
|
20
20
|
export declare function detectBaseDir(cwd?: string): string;
|
|
21
21
|
export declare function createCapsuleFiles(options: GeneratorOptions): CapsuleResult;
|
|
22
|
-
export declare function
|
|
23
|
-
export declare function
|
|
22
|
+
export declare function printGenerateHelp(): void;
|
|
23
|
+
export declare function runGenerateWizard(rawArgs?: string[]): Promise<any>;
|
|
24
|
+
export declare function runGenerateCapsule(capsuleName: string): Promise<any>;
|
package/cli/generator.js
CHANGED
|
@@ -193,7 +193,55 @@ export const createCapsuleFiles = ({
|
|
|
193
193
|
};
|
|
194
194
|
};
|
|
195
195
|
|
|
196
|
+
export const printGenerateHelp = () => {
|
|
197
|
+
renderBanner('Chemical X: Capsule Generator Usage');
|
|
198
|
+
const BOLD = '\x1b[1m';
|
|
199
|
+
const CYAN = '\x1b[36m';
|
|
200
|
+
const DIM = '\x1b[2m';
|
|
201
|
+
const RESET = '\x1b[0m';
|
|
202
|
+
|
|
203
|
+
const out = [
|
|
204
|
+
`${BOLD}USAGE${RESET}`,
|
|
205
|
+
` ${CYAN}npx chemx generate${RESET} <name> [options]`,
|
|
206
|
+
` ${CYAN}npx chemx generate${RESET} <tier> <name> [options]`,
|
|
207
|
+
'',
|
|
208
|
+
`${BOLD}TIERS${RESET}`,
|
|
209
|
+
` ${CYAN}atom${RESET} (a-) Foundational UI elements (raw HTML permitted)`,
|
|
210
|
+
` ${CYAN}molecule${RESET} (m-) Groups of atoms (no raw HTML, co-located controller)`,
|
|
211
|
+
` ${CYAN}organism${RESET} (o-) Complex feature modules`,
|
|
212
|
+
` ${CYAN}hook${RESET} (use-) Domain state / headless composables (< 5 return properties)`,
|
|
213
|
+
` ${CYAN}view${RESET} (v-) Declarative Table-of-Contents views`,
|
|
214
|
+
'',
|
|
215
|
+
`${BOLD}OPTIONS${RESET}`,
|
|
216
|
+
` ${CYAN}--tier=<tier>${RESET} Specify component tier`,
|
|
217
|
+
` ${CYAN}--framework=<react|vue|svelte>${RESET} Framework flavor (default: auto-detected or react)`,
|
|
218
|
+
` ${CYAN}--dir=<path>${RESET} Target directory (default: src/components/<tier>s)`,
|
|
219
|
+
` ${CYAN}--lean${RESET} Generate minimal capsule without controller/spec`,
|
|
220
|
+
` ${CYAN}--json${RESET} Output result as minified JSON`,
|
|
221
|
+
` ${CYAN}-y, --yes${RESET} Non-interactive mode with defaults`,
|
|
222
|
+
` ${CYAN}-h, --help${RESET} Show this help message`,
|
|
223
|
+
'',
|
|
224
|
+
`${BOLD}EXAMPLES${RESET}`,
|
|
225
|
+
` ${CYAN}npx chemx generate m-task-list --framework=react${RESET}`,
|
|
226
|
+
` ${CYAN}npx chemx generate hook use-task-filter${RESET}`,
|
|
227
|
+
` ${CYAN}npx chemx generate atom badge --dir=src/ui/atoms${RESET}`,
|
|
228
|
+
''
|
|
229
|
+
].join('\n');
|
|
230
|
+
|
|
231
|
+
process.stdout.write(out);
|
|
232
|
+
};
|
|
233
|
+
|
|
196
234
|
export const runGenerateWizard = async (rawArgs = []) => {
|
|
235
|
+
const isHelp = rawArgs.includes('--help') || rawArgs.includes('-h') || rawArgs.includes('help');
|
|
236
|
+
if (isHelp) {
|
|
237
|
+
if (rawArgs.includes('--json')) {
|
|
238
|
+
process.stdout.write(JSON.stringify({ help: true, success: true }) + '\n');
|
|
239
|
+
} else {
|
|
240
|
+
printGenerateHelp();
|
|
241
|
+
}
|
|
242
|
+
return { success: true, help: true };
|
|
243
|
+
}
|
|
244
|
+
|
|
197
245
|
const isJson = rawArgs.includes('--json');
|
|
198
246
|
const isYes = rawArgs.includes('-y') || rawArgs.includes('--yes') || !process.stdin.isTTY;
|
|
199
247
|
if (!isJson) renderBanner('Chemical X: Molecular Capsule Wizard');
|
|
@@ -225,6 +273,17 @@ export const runGenerateWizard = async (rawArgs = []) => {
|
|
|
225
273
|
rawName = useGum
|
|
226
274
|
? gumInput('Capsule feature name (e.g. user-avatar, spark-kpi, auth-status):', 'user-avatar')
|
|
227
275
|
: await promptQuestion('Capsule feature name [user-avatar]: ');
|
|
276
|
+
} else if (!rawName && isYes) {
|
|
277
|
+
const hasExplicitFlags = rawArgs.some((a) => a.startsWith('-'));
|
|
278
|
+
if (hasExplicitFlags) {
|
|
279
|
+
const err = new Error('Missing capsule name. Usage: npx chemx generate <name> [options]');
|
|
280
|
+
if (isJson) {
|
|
281
|
+
process.stdout.write(JSON.stringify({ error: err.message, success: false }) + '\n');
|
|
282
|
+
process.exit(1);
|
|
283
|
+
}
|
|
284
|
+
process.stderr.write(`\x1b[31m✕ Error: ${err.message}\x1b[0m\n`);
|
|
285
|
+
process.exit(1);
|
|
286
|
+
}
|
|
228
287
|
}
|
|
229
288
|
const cleanName = (rawName || 'user-avatar').trim().toLowerCase();
|
|
230
289
|
const selectedTier = resolveSelectedTier(explicitTier, cleanName);
|
package/cli/generator.spec.js
CHANGED
|
@@ -182,4 +182,21 @@ test('obtainLicenseKey: returns null immediately when --headless is passed witho
|
|
|
182
182
|
assert.strictEqual(key, null);
|
|
183
183
|
});
|
|
184
184
|
|
|
185
|
+
test('runGenerateWizard: --help displays help and never writes files to disk', async () => {
|
|
186
|
+
const tmpDir = path.resolve(process.cwd(), 'scratch/test-gen-help');
|
|
187
|
+
if (fs.existsSync(tmpDir)) fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
188
|
+
|
|
189
|
+
const res = await runGenerateWizard(['--help', `--dir=${tmpDir}`]);
|
|
190
|
+
assert.strictEqual(res.success, true);
|
|
191
|
+
assert.strictEqual(res.help, true);
|
|
192
|
+
assert.strictEqual(fs.existsSync(tmpDir), false);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test('runGenerateWizard: --json with --help returns help object without mutation', async () => {
|
|
196
|
+
const res = await runGenerateWizard(['--help', '--json']);
|
|
197
|
+
assert.strictEqual(res.success, true);
|
|
198
|
+
assert.strictEqual(res.help, true);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
|
|
185
202
|
|
package/cli/help.js
CHANGED
|
@@ -129,6 +129,24 @@ export const printHelp = () => {
|
|
|
129
129
|
` Registers in .cursor/mcp.json, .vscode/mcp.json, and ~/.gemini/config/mcp_config.json.`,
|
|
130
130
|
` Example: ${CYAN}npx chemx install-mcp${RESET}`,
|
|
131
131
|
'',
|
|
132
|
+
` ${CYAN}team${RESET} [subcommand] [options] ${DIM}(aliases: swarm, feed)${RESET}`,
|
|
133
|
+
` Database-first multi-agent coordination layer powered by SQLite (.chemx/index.db).`,
|
|
134
|
+
` Manages task queues, agent states, distributed file locks, and swarm communication`,
|
|
135
|
+
` with zero monolithic markdown context bloat.`,
|
|
136
|
+
` Subcommands:`,
|
|
137
|
+
` ${DIM}- ${RESET}${CYAN}status${RESET} Swarm metrics, active agent states, file locks, and token accounting`,
|
|
138
|
+
` ${DIM}- ${RESET}${CYAN}feed${RESET} Social broadcast feed and agent event stream`,
|
|
139
|
+
` ${DIM}- ${RESET}${CYAN}task${RESET} Asana-style task backlog (list, claim, update, triage)`,
|
|
140
|
+
` ${DIM}- ${RESET}${CYAN}lock${RESET} Distributed file lock queue preventing concurrent write collisions`,
|
|
141
|
+
` Flags: ${CYAN}--json${RESET}, ${CYAN}--status=queued|in_progress|done${RESET}, ${CYAN}--agent=@id${RESET}, ${CYAN}--as=@id${RESET}`,
|
|
142
|
+
` Examples: ${CYAN}npx chemx team status${RESET} or ${CYAN}npx chemx team task list${RESET}`,
|
|
143
|
+
'',
|
|
144
|
+
` ${CYAN}ui${RESET} [options] ${DIM}(aliases: preview, dashboard)${RESET}`,
|
|
145
|
+
` Launch the local Chemical X developer studio web server.`,
|
|
146
|
+
` Serves interactive Kanban board, classic forum feed, SQLite DB studio, and AST filetree.`,
|
|
147
|
+
` Flags: ${CYAN}--port=<port>${RESET} (default: 4173)`,
|
|
148
|
+
` Example: ${CYAN}npx chemx ui${RESET}`,
|
|
149
|
+
'',
|
|
132
150
|
` ${CYAN}--version${RESET}, ${CYAN}-v${RESET}, ${CYAN}version${RESET}`,
|
|
133
151
|
` Display current Chemical X package version string.`,
|
|
134
152
|
'',
|
|
@@ -90,19 +90,17 @@ test('installProjectMcpConfig: creates .cursor and .vscode configs and updates p
|
|
|
90
90
|
}
|
|
91
91
|
});
|
|
92
92
|
|
|
93
|
-
test('syncAntigravityMcpSchemas: writes
|
|
93
|
+
test('syncAntigravityMcpSchemas: writes master gateway schema and instructions.md', () => {
|
|
94
94
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'chemx-antigravity-test-'));
|
|
95
95
|
try {
|
|
96
96
|
const res = syncAntigravityMcpSchemas(tmpDir, { silent: true });
|
|
97
97
|
assert.strictEqual(res.toolCount, MCP_TOOLS.length);
|
|
98
|
-
assert.ok(fs.existsSync(path.join(tmpDir, '
|
|
99
|
-
assert.ok(fs.existsSync(path.join(tmpDir, 'chemx_typecheck.json')));
|
|
100
|
-
assert.ok(fs.existsSync(path.join(tmpDir, 'chemx_test.json')));
|
|
98
|
+
assert.ok(fs.existsSync(path.join(tmpDir, 'chemx.json')));
|
|
101
99
|
assert.ok(fs.existsSync(path.join(tmpDir, 'instructions.md')));
|
|
102
100
|
|
|
103
101
|
const instructions = fs.readFileSync(path.join(tmpDir, 'instructions.md'), 'utf-8');
|
|
104
|
-
assert.ok(instructions.includes('
|
|
105
|
-
assert.ok(instructions.includes('
|
|
102
|
+
assert.ok(instructions.includes('chemx'));
|
|
103
|
+
assert.ok(instructions.includes('MASTER TOOL'));
|
|
106
104
|
} finally {
|
|
107
105
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
108
106
|
}
|
package/cli/patcher.js
CHANGED
|
@@ -151,6 +151,28 @@ export const patchFile = (targetPath, params = {}) => {
|
|
|
151
151
|
* @param {boolean} isCli Whether invoked directly from CLI.
|
|
152
152
|
*/
|
|
153
153
|
export const runPatcherCli = (args, isCli = false) => {
|
|
154
|
+
if (args.includes('--help') || args.includes('-h') || args.includes('help')) {
|
|
155
|
+
const isJson = args.includes('--json');
|
|
156
|
+
if (isJson) {
|
|
157
|
+
process.stdout.write(JSON.stringify({ help: true, success: true }) + '\n');
|
|
158
|
+
} else {
|
|
159
|
+
process.stdout.write([
|
|
160
|
+
`${ANSI.BOLD}USAGE${ANSI.RESET}`,
|
|
161
|
+
` chemx patch <file> --target="text" --replacement="new" [options]`,
|
|
162
|
+
'',
|
|
163
|
+
`${ANSI.BOLD}OPTIONS${ANSI.RESET}`,
|
|
164
|
+
` --target="<text>" Exact text block to replace`,
|
|
165
|
+
` --replacement="<new>" New replacement content`,
|
|
166
|
+
` --multiple Allow replacing multiple occurrences`,
|
|
167
|
+
` --json Output result as minified JSON`,
|
|
168
|
+
` -h, --help Show this help message`,
|
|
169
|
+
''
|
|
170
|
+
].join('\n'));
|
|
171
|
+
}
|
|
172
|
+
if (isCli) process.exit(0);
|
|
173
|
+
return { help: true, success: true };
|
|
174
|
+
}
|
|
175
|
+
|
|
154
176
|
const nonFlagArgs = args.filter((a) => !a.startsWith('-'));
|
|
155
177
|
const filePath = nonFlagArgs[0];
|
|
156
178
|
|
|
@@ -317,6 +339,26 @@ export const writeFile = (targetPath, params = {}) => {
|
|
|
317
339
|
* @param {boolean} isCli Whether invoked directly from CLI.
|
|
318
340
|
*/
|
|
319
341
|
export const runWriterCli = (args, isCli = false) => {
|
|
342
|
+
if (args.includes('--help') || args.includes('-h') || args.includes('help')) {
|
|
343
|
+
const isJson = args.includes('--json');
|
|
344
|
+
if (isJson) {
|
|
345
|
+
process.stdout.write(JSON.stringify({ help: true, success: true }) + '\n');
|
|
346
|
+
} else {
|
|
347
|
+
process.stdout.write([
|
|
348
|
+
`${ANSI.BOLD}USAGE${ANSI.RESET}`,
|
|
349
|
+
` chemx write <file> --content="text" [options]`,
|
|
350
|
+
'',
|
|
351
|
+
`${ANSI.BOLD}OPTIONS${ANSI.RESET}`,
|
|
352
|
+
` --content="<text>" File content to write`,
|
|
353
|
+
` --json Output result as minified JSON`,
|
|
354
|
+
` -h, --help Show this help message`,
|
|
355
|
+
''
|
|
356
|
+
].join('\n'));
|
|
357
|
+
}
|
|
358
|
+
if (isCli) process.exit(0);
|
|
359
|
+
return { help: true, success: true };
|
|
360
|
+
}
|
|
361
|
+
|
|
320
362
|
const nonFlagArgs = args.filter((a) => !a.startsWith('-'));
|
|
321
363
|
const filePath = nonFlagArgs[0];
|
|
322
364
|
|
package/cli/patcher.spec.js
CHANGED
|
@@ -146,3 +146,22 @@ test('patchFile: detects Directive 1.G raw DOM violations in molecule files', ()
|
|
|
146
146
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
147
147
|
}
|
|
148
148
|
});
|
|
149
|
+
|
|
150
|
+
test('runPatcherCli: --help displays usage and returns success without error', () => {
|
|
151
|
+
import('./patcher.js').then(({ runPatcherCli, runWriterCli }) => {
|
|
152
|
+
const patchRes = runPatcherCli(['--help'], false);
|
|
153
|
+
assert.strictEqual(patchRes.success, true);
|
|
154
|
+
assert.strictEqual(patchRes.help, true);
|
|
155
|
+
|
|
156
|
+
const writeRes = runWriterCli(['--help'], false);
|
|
157
|
+
assert.strictEqual(writeRes.success, true);
|
|
158
|
+
assert.strictEqual(writeRes.help, true);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test('ANSI.GREEN: exists and is defined in theme.js', async () => {
|
|
163
|
+
const { ANSI } = await import('./theme.js');
|
|
164
|
+
assert.notStrictEqual(ANSI.GREEN, undefined);
|
|
165
|
+
assert.ok(typeof ANSI.GREEN === 'string');
|
|
166
|
+
});
|
|
167
|
+
|
package/cli/reader.js
CHANGED
|
@@ -342,6 +342,31 @@ export const readTokenOptimized = (targetPath, options = {}) => {
|
|
|
342
342
|
* @param {boolean} isCli Whether invoked directly from CLI.
|
|
343
343
|
*/
|
|
344
344
|
export const runReaderCli = (args, isCli = false) => {
|
|
345
|
+
if (args.includes('--help') || args.includes('-h') || args.includes('help')) {
|
|
346
|
+
const isJson = args.includes('--json');
|
|
347
|
+
if (isJson) {
|
|
348
|
+
process.stdout.write(JSON.stringify({ help: true, success: true }) + '\n');
|
|
349
|
+
} else {
|
|
350
|
+
process.stdout.write([
|
|
351
|
+
`${ANSI.BOLD}USAGE${ANSI.RESET}`,
|
|
352
|
+
` chemx read <file> [options]`,
|
|
353
|
+
'',
|
|
354
|
+
`${ANSI.BOLD}OPTIONS${ANSI.RESET}`,
|
|
355
|
+
` --outline AST structure outline only (80%+ token savings)`,
|
|
356
|
+
` --symbol=<name> Extract specific function/hook/interface declaration`,
|
|
357
|
+
` --start=<N> Start line number (1-indexed)`,
|
|
358
|
+
` --end=<N> End line number (1-indexed)`,
|
|
359
|
+
` --strip-comments Remove comments to minimize tokens`,
|
|
360
|
+
` --compact Collapse empty whitespace lines`,
|
|
361
|
+
` --json Output result as minified JSON`,
|
|
362
|
+
` -h, --help Show this help message`,
|
|
363
|
+
''
|
|
364
|
+
].join('\n'));
|
|
365
|
+
}
|
|
366
|
+
if (isCli) process.exit(0);
|
|
367
|
+
return { help: true, success: true };
|
|
368
|
+
}
|
|
369
|
+
|
|
345
370
|
const nonFlagArgs = args.filter((a) => !a.startsWith('-'));
|
|
346
371
|
const filePath = nonFlagArgs[0];
|
|
347
372
|
|
package/cli/scaffold.js
CHANGED
|
@@ -13,10 +13,10 @@ export const runScaffold = async (projectName, rawArgs = [], onRunAudit = null)
|
|
|
13
13
|
if (!licenseKey) {
|
|
14
14
|
process.stdout.write(
|
|
15
15
|
'\n\x1b[38;2;98;201;255m⚡ Chemical X: Community Edition (Free)\x1b[0m\n' +
|
|
16
|
-
'
|
|
17
|
-
'
|
|
18
|
-
'
|
|
19
|
-
'
|
|
16
|
+
'Open-source molecular architecture standard for high-velocity AI coding.\n' +
|
|
17
|
+
'Scorecards can be shared with the community via PR checks or the share menu.\n' +
|
|
18
|
+
'No background telemetry is exfiltrated without your explicit consent.\n' +
|
|
19
|
+
'Thank you for supporting Chemical X!\n\n'
|
|
20
20
|
);
|
|
21
21
|
files = loadLocalBlueprintFiles();
|
|
22
22
|
isCommunity = true;
|
|
@@ -57,17 +57,26 @@ export const runScaffold = async (projectName, rawArgs = [], onRunAudit = null)
|
|
|
57
57
|
process.exit(1);
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
const resolveScaffoldTarget = (rel) => {
|
|
61
|
+
if (rel === '_package.json') return 'package.json';
|
|
62
|
+
if (rel === '_tsconfig.json') return 'tsconfig.json';
|
|
63
|
+
if (rel === '_vitest.config.ts') return 'vitest.config.ts';
|
|
64
|
+
if (rel === '_gitignore') return '.gitignore';
|
|
65
|
+
return rel;
|
|
66
|
+
};
|
|
67
|
+
|
|
60
68
|
process.stdout.write(`Scaffolding Molecular Architecture into: \x1b[36m${finalDirName}/\x1b[0m\n`);
|
|
61
69
|
fs.mkdirSync(targetDir, { recursive: true });
|
|
62
70
|
|
|
63
71
|
for (const [relPath, content] of Object.entries(files)) {
|
|
64
|
-
const
|
|
72
|
+
const targetRel = resolveScaffoldTarget(relPath);
|
|
73
|
+
const fullPath = path.join(targetDir, targetRel);
|
|
65
74
|
const dirName = path.dirname(fullPath);
|
|
66
75
|
if (!fs.existsSync(dirName)) {
|
|
67
76
|
fs.mkdirSync(dirName, { recursive: true });
|
|
68
77
|
}
|
|
69
78
|
fs.writeFileSync(fullPath, content, 'utf-8');
|
|
70
|
-
process.stdout.write(` \x1b[32m✔\x1b[0m ${
|
|
79
|
+
process.stdout.write(` \x1b[32m✔\x1b[0m ${targetRel}\n`);
|
|
71
80
|
}
|
|
72
81
|
|
|
73
82
|
const cursorRulesPath = path.join(targetDir, '.cursorrules');
|
|
@@ -83,6 +92,13 @@ export const runScaffold = async (projectName, rawArgs = [], onRunAudit = null)
|
|
|
83
92
|
process.stdout.write(' \x1b[32m✔\x1b[0m .cursorrules\n');
|
|
84
93
|
}
|
|
85
94
|
|
|
95
|
+
const agentsMdSrc = path.resolve(path.dirname(new URL(import.meta.url).pathname), '../AGENTS.md');
|
|
96
|
+
const agentsMdDest = path.join(targetDir, 'AGENTS.md');
|
|
97
|
+
if (fs.existsSync(agentsMdSrc) && !fs.existsSync(agentsMdDest)) {
|
|
98
|
+
fs.copyFileSync(agentsMdSrc, agentsMdDest);
|
|
99
|
+
process.stdout.write(' \x1b[32m✔\x1b[0m AGENTS.md\n');
|
|
100
|
+
}
|
|
101
|
+
|
|
86
102
|
process.stdout.write(
|
|
87
103
|
`\n\x1b[1m\x1b[32m✔ Molecular Architecture project created successfully at ${finalDirName}!\x1b[0m\n\n`
|
|
88
104
|
);
|
|
@@ -90,7 +106,8 @@ export const runScaffold = async (projectName, rawArgs = [], onRunAudit = null)
|
|
|
90
106
|
process.stdout.write(` 1. cd ${finalDirName}\n`);
|
|
91
107
|
process.stdout.write(' 2. Review AGENTS.md for line budgets and architecture standards\n');
|
|
92
108
|
process.stdout.write(' 3. Run npx chemx generate m-<feature> to create capsules\n');
|
|
93
|
-
process.stdout.write(' 4. Run npx chemx audit to scan for line budget compliance\n
|
|
109
|
+
process.stdout.write(' 4. Run npx chemx audit to scan for line budget compliance\n');
|
|
110
|
+
process.stdout.write(' 5. Run npx chemx verify to verify AST rules, typecheck, and tests\n\n');
|
|
94
111
|
};
|
|
95
112
|
|
|
96
113
|
export const runInit = async (targetSubDir = 'src/chemical-x', rawArgs = [], onRunAudit = null) => {
|
|
@@ -114,6 +131,9 @@ export const runInit = async (targetSubDir = 'src/chemical-x', rawArgs = [], onR
|
|
|
114
131
|
|
|
115
132
|
let count = 0;
|
|
116
133
|
for (const [relPath, content] of Object.entries(files)) {
|
|
134
|
+
if (relPath === '_package.json' || relPath === '_tsconfig.json' || relPath === '_vitest.config.ts') {
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
117
137
|
const fullPath = path.join(targetDir, relPath);
|
|
118
138
|
const dirName = path.dirname(fullPath);
|
|
119
139
|
if (!fs.existsSync(dirName)) {
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
4
|
+
import { initTeamSchema } from './team-schema.js';
|
|
5
|
+
import {
|
|
6
|
+
MODEL_PRICING,
|
|
7
|
+
DEFAULT_MODEL,
|
|
8
|
+
resolveModelPricing,
|
|
9
|
+
calculateCost,
|
|
10
|
+
ingestTaskTelemetry
|
|
11
|
+
} from './team-telemetry.js';
|
|
12
|
+
|
|
13
|
+
const setupTestDb = () => {
|
|
14
|
+
const db = new DatabaseSync(':memory:');
|
|
15
|
+
initTeamSchema(db);
|
|
16
|
+
return db;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
test('team-telemetry: rate cards defined for all canonical models', () => {
|
|
20
|
+
assert.deepStrictEqual(MODEL_PRICING['frontier-blended'], { prompt: 0.0000025, completion: 0.0000100 });
|
|
21
|
+
assert.deepStrictEqual(MODEL_PRICING['claude-3-5-sonnet'], { prompt: 0.0000030, completion: 0.0000150 });
|
|
22
|
+
assert.deepStrictEqual(MODEL_PRICING['gpt-4o'], { prompt: 0.0000025, completion: 0.0000100 });
|
|
23
|
+
assert.deepStrictEqual(MODEL_PRICING['local'], { prompt: 0.0000000, completion: 0.0000000 });
|
|
24
|
+
assert.strictEqual(DEFAULT_MODEL, 'frontier-blended');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('team-telemetry: resolveModelPricing falls back to frontier-blended', () => {
|
|
28
|
+
const resolvedClaude = resolveModelPricing('claude-3-5-sonnet-20241022');
|
|
29
|
+
assert.strictEqual(resolvedClaude.prompt, 0.0000030);
|
|
30
|
+
|
|
31
|
+
const resolvedGpt = resolveModelPricing('gpt-4o-mini');
|
|
32
|
+
assert.strictEqual(resolvedGpt.prompt, 0.0000025);
|
|
33
|
+
|
|
34
|
+
const resolvedLocal = resolveModelPricing('local-llama-3');
|
|
35
|
+
assert.strictEqual(resolvedLocal.prompt, 0.0);
|
|
36
|
+
|
|
37
|
+
const resolvedUnknown = resolveModelPricing('custom-exotic-model');
|
|
38
|
+
assert.strictEqual(resolvedUnknown.prompt, 0.0000025);
|
|
39
|
+
|
|
40
|
+
const resolvedEmpty = resolveModelPricing('');
|
|
41
|
+
assert.strictEqual(resolvedEmpty.prompt, 0.0000025);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('team-telemetry: calculateCost accurately derives micro-USD costs', () => {
|
|
45
|
+
const costDefault = calculateCost(1000, 1000);
|
|
46
|
+
assert.strictEqual(costDefault, 0.0125);
|
|
47
|
+
|
|
48
|
+
const costClaude = calculateCost(1000, 1000, 'claude-3-5-sonnet');
|
|
49
|
+
assert.strictEqual(costClaude, 0.018);
|
|
50
|
+
|
|
51
|
+
const costLocal = calculateCost(5000, 5000, 'local');
|
|
52
|
+
assert.strictEqual(costLocal, 0.0);
|
|
53
|
+
|
|
54
|
+
const costObject = calculateCost({ promptTokens: 2000, completionTokens: 1000, model: 'claude-3-5-sonnet' });
|
|
55
|
+
assert.strictEqual(costObject, 0.021);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('team-telemetry: ingestTaskTelemetry applies model pricing card to database records', () => {
|
|
59
|
+
const db = setupTestDb();
|
|
60
|
+
const now = Date.now();
|
|
61
|
+
db.prepare("INSERT INTO agents (id, name, role, heartbeat) VALUES ('@worker-m1', 'Worker M1', 'implementer', ?)").run(now);
|
|
62
|
+
const taskRes = db.prepare("INSERT INTO agent_tasks (title, tier, status, created_at, updated_at) VALUES ('Test M1 Task', 'backend', 'done', ?, ?)").run(now, now);
|
|
63
|
+
const taskId = Number(taskRes.lastInsertRowid);
|
|
64
|
+
|
|
65
|
+
const result = ingestTaskTelemetry(db, taskId, '@worker-m1', {
|
|
66
|
+
model: 'claude-3-5-sonnet',
|
|
67
|
+
tokens: { prompt: 2000, completion: 1000 }
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
assert.ok(result);
|
|
71
|
+
assert.strictEqual(result.prompt_tokens, 2000);
|
|
72
|
+
assert.strictEqual(result.completion_tokens, 1000);
|
|
73
|
+
assert.strictEqual(result.cost_usd, 0.021);
|
|
74
|
+
|
|
75
|
+
const taskRow = db.prepare('SELECT cost_usd, prompt_tokens, completion_tokens FROM agent_tasks WHERE id = ?').get(taskId);
|
|
76
|
+
assert.strictEqual(taskRow.cost_usd, 0.021);
|
|
77
|
+
|
|
78
|
+
const agentRow = db.prepare('SELECT total_cost_usd FROM agents WHERE id = ?').get('@worker-m1');
|
|
79
|
+
assert.strictEqual(agentRow.total_cost_usd, 0.021);
|
|
80
|
+
});
|
package/cli/team/team-schema.js
CHANGED
|
@@ -57,6 +57,12 @@ export const initTeamSchema = (db) => {
|
|
|
57
57
|
requested_at INTEGER NOT NULL, status TEXT NOT NULL DEFAULT 'waiting',
|
|
58
58
|
priority INTEGER NOT NULL DEFAULT 2, purpose TEXT NOT NULL DEFAULT ''
|
|
59
59
|
);
|
|
60
|
+
CREATE TABLE IF NOT EXISTS forum_topics (
|
|
61
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT, category_id TEXT NOT NULL, title TEXT NOT NULL,
|
|
62
|
+
feature_tag TEXT NOT NULL DEFAULT '', author_id TEXT NOT NULL, created_at INTEGER NOT NULL,
|
|
63
|
+
updated_at INTEGER NOT NULL, pinned INTEGER NOT NULL DEFAULT 0
|
|
64
|
+
);
|
|
65
|
+
CREATE INDEX IF NOT EXISTS idx_forum_topics_cat ON forum_topics(category_id);
|
|
60
66
|
CREATE INDEX IF NOT EXISTS idx_agent_tasks_status ON agent_tasks(status);
|
|
61
67
|
CREATE INDEX IF NOT EXISTS idx_agent_tasks_agent ON agent_tasks(assigned_agent_id);
|
|
62
68
|
CREATE INDEX IF NOT EXISTS idx_agent_tasks_path ON agent_tasks(target_path);
|
|
@@ -1,12 +1,16 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Chemical X Protocol: Token Telemetry Tracking & Automated Ingestion
|
|
3
|
-
* Parses runtime transcript logs or structured options to record token telemetry
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
1
|
import fs from 'node:fs';
|
|
7
2
|
import path from 'node:path';
|
|
8
3
|
import { normalizeAgentId } from './team-db-task-helpers.js';
|
|
9
4
|
|
|
5
|
+
export const MODEL_PRICING = {
|
|
6
|
+
'frontier-blended': { prompt: 0.0000025, completion: 0.0000100 },
|
|
7
|
+
'claude-3-5-sonnet': { prompt: 0.0000030, completion: 0.0000150 },
|
|
8
|
+
'gpt-4o': { prompt: 0.0000025, completion: 0.0000100 },
|
|
9
|
+
'local': { prompt: 0.0000000, completion: 0.0000000 }
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const DEFAULT_MODEL = 'frontier-blended';
|
|
13
|
+
|
|
10
14
|
const toResultSync = (operation) => {
|
|
11
15
|
try {
|
|
12
16
|
return [operation(), null];
|
|
@@ -15,13 +19,40 @@ const toResultSync = (operation) => {
|
|
|
15
19
|
}
|
|
16
20
|
};
|
|
17
21
|
|
|
22
|
+
export const resolveModelPricing = (modelId = '') => {
|
|
23
|
+
const hasModelId = typeof modelId === 'string' && Boolean(modelId);
|
|
24
|
+
if (!hasModelId) return MODEL_PRICING[DEFAULT_MODEL];
|
|
25
|
+
const norm = modelId.toLowerCase().trim();
|
|
26
|
+
const hasExact = Object.prototype.hasOwnProperty.call(MODEL_PRICING, norm);
|
|
27
|
+
if (hasExact) return MODEL_PRICING[norm];
|
|
28
|
+
const isClaude = norm.includes('claude-3-5-sonnet') || norm.includes('claude-3.5-sonnet');
|
|
29
|
+
if (isClaude) return MODEL_PRICING['claude-3-5-sonnet'];
|
|
30
|
+
const isGpt = norm.includes('gpt-4o');
|
|
31
|
+
if (isGpt) return MODEL_PRICING['gpt-4o'];
|
|
32
|
+
const isLocal = norm.includes('local') || norm.includes('ollama') || norm.includes('llama');
|
|
33
|
+
if (isLocal) return MODEL_PRICING['local'];
|
|
34
|
+
return MODEL_PRICING[DEFAULT_MODEL];
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const calculateCost = (promptTokens = 0, completionTokens = 0, modelId = DEFAULT_MODEL) => {
|
|
38
|
+
const isObjectCall = typeof promptTokens === 'object' && promptTokens !== null;
|
|
39
|
+
if (isObjectCall) {
|
|
40
|
+
const opts = promptTokens;
|
|
41
|
+
const p = Number(opts.prompt ?? opts.promptTokens ?? opts.prompt_tokens ?? 0);
|
|
42
|
+
const c = Number(opts.completion ?? opts.completionTokens ?? opts.completion_tokens ?? 0);
|
|
43
|
+
const m = opts.model || opts.modelId || DEFAULT_MODEL;
|
|
44
|
+
return calculateCost(p, c, m);
|
|
45
|
+
}
|
|
46
|
+
const pricing = resolveModelPricing(modelId);
|
|
47
|
+
const totalCost = (Number(promptTokens || 0) * pricing.prompt) + (Number(completionTokens || 0) * pricing.completion);
|
|
48
|
+
return Number(totalCost.toFixed(6));
|
|
49
|
+
};
|
|
50
|
+
|
|
18
51
|
export const findTranscriptLog = (options = {}) => {
|
|
19
|
-
|
|
52
|
+
const hasExplicitPath = Boolean(options.logPath && fs.existsSync(options.logPath));
|
|
53
|
+
if (hasExplicitPath) return options.logPath;
|
|
20
54
|
const cwd = options.cwd || process.cwd();
|
|
21
|
-
const candidates = [
|
|
22
|
-
path.resolve(cwd, '.chemx/logs/transcript.jsonl'),
|
|
23
|
-
path.resolve(cwd, '.system_generated/logs/transcript.jsonl')
|
|
24
|
-
];
|
|
55
|
+
const candidates = [path.resolve(cwd, '.chemx/logs/transcript.jsonl'), path.resolve(cwd, '.system_generated/logs/transcript.jsonl')];
|
|
25
56
|
for (const p of candidates) if (fs.existsSync(p)) return p;
|
|
26
57
|
const brainDir = '/home/xopher/.gemini/antigravity/brain';
|
|
27
58
|
if (fs.existsSync(brainDir)) {
|
|
@@ -35,18 +66,10 @@ export const findTranscriptLog = (options = {}) => {
|
|
|
35
66
|
|
|
36
67
|
const parseTokensFromEntry = (entry) => {
|
|
37
68
|
if (entry.tokens) {
|
|
38
|
-
return [
|
|
39
|
-
Number(entry.tokens.prompt ?? entry.tokens.prompt_tokens ?? 0),
|
|
40
|
-
Number(entry.tokens.completion ?? entry.tokens.completion_tokens ?? 0),
|
|
41
|
-
Number(entry.tokens.cached ?? entry.tokens.cached_tokens ?? 0)
|
|
42
|
-
];
|
|
69
|
+
return [Number(entry.tokens.prompt ?? entry.tokens.prompt_tokens ?? 0), Number(entry.tokens.completion ?? entry.tokens.completion_tokens ?? 0), Number(entry.tokens.cached ?? entry.tokens.cached_tokens ?? 0)];
|
|
43
70
|
}
|
|
44
71
|
if (entry.usage) {
|
|
45
|
-
return [
|
|
46
|
-
Number(entry.usage.prompt_tokens ?? entry.usage.input_tokens ?? 0),
|
|
47
|
-
Number(entry.usage.completion_tokens ?? entry.usage.output_tokens ?? 0),
|
|
48
|
-
Number(entry.usage.cached_tokens ?? 0)
|
|
49
|
-
];
|
|
72
|
+
return [Number(entry.usage.prompt_tokens ?? entry.usage.input_tokens ?? 0), Number(entry.usage.completion_tokens ?? entry.usage.output_tokens ?? 0), Number(entry.usage.cached_tokens ?? 0)];
|
|
50
73
|
}
|
|
51
74
|
const textLen = (entry.content || '').length;
|
|
52
75
|
const toolLen = entry.tool_calls ? JSON.stringify(entry.tool_calls).length : 0;
|
|
@@ -55,30 +78,31 @@ const parseTokensFromEntry = (entry) => {
|
|
|
55
78
|
return isModel ? [0, est, 0] : [est, 0, 0];
|
|
56
79
|
};
|
|
57
80
|
|
|
58
|
-
export const parseTranscriptFile = (filePath) => {
|
|
81
|
+
export const parseTranscriptFile = (filePath, options = {}) => {
|
|
59
82
|
let [p, c, k] = [0, 0, 0];
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const
|
|
64
|
-
for (const line of lines) {
|
|
83
|
+
const fileExists = Boolean(filePath && fs.existsSync(filePath));
|
|
84
|
+
if (!fileExists) return { promptTokens: 0, completionTokens: 0, cachedTokens: 0, totalTokens: 0, costUsd: 0 };
|
|
85
|
+
let detectedModel = options.model || options.modelId || null;
|
|
86
|
+
for (const line of fs.readFileSync(filePath, 'utf8').split('\n')) {
|
|
65
87
|
if (!line.trim()) continue;
|
|
66
88
|
const [parsed, parseErr] = toResultSync(() => JSON.parse(line));
|
|
67
|
-
const
|
|
68
|
-
if (!
|
|
89
|
+
const isValid = Boolean(parsed) && !parseErr;
|
|
90
|
+
if (!isValid) continue;
|
|
91
|
+
const entryModel = parsed.model || parsed.model_id || parsed.modelId;
|
|
92
|
+
const canAdoptModel = !detectedModel && Boolean(entryModel);
|
|
93
|
+
if (canAdoptModel) detectedModel = entryModel;
|
|
69
94
|
const [dp, dc, dk] = parseTokensFromEntry(parsed);
|
|
70
95
|
p += dp; c += dc; k += dk;
|
|
71
96
|
}
|
|
72
97
|
const tot = p + c;
|
|
73
|
-
const
|
|
74
|
-
return { promptTokens: p, completionTokens: c, cachedTokens: k, totalTokens: tot, costUsd
|
|
98
|
+
const costUsd = calculateCost(p, c, detectedModel || DEFAULT_MODEL);
|
|
99
|
+
return { promptTokens: p, completionTokens: c, cachedTokens: k, totalTokens: tot, costUsd };
|
|
75
100
|
};
|
|
76
101
|
|
|
77
102
|
export const ingestTaskTelemetry = (db, taskId, agentId, options = {}) => {
|
|
78
|
-
const
|
|
79
|
-
const hasTaskId = Boolean(taskId);
|
|
80
|
-
const canIngest = hasDb && hasTaskId;
|
|
103
|
+
const canIngest = Boolean(db && taskId);
|
|
81
104
|
if (!canIngest) return null;
|
|
105
|
+
const model = options.model || options.modelId || options.model_id || DEFAULT_MODEL;
|
|
82
106
|
let p = 0, c = 0, k = 0, tot = 0, cost = 0.0;
|
|
83
107
|
if (options.tokens) {
|
|
84
108
|
const t = options.tokens;
|
|
@@ -86,19 +110,18 @@ export const ingestTaskTelemetry = (db, taskId, agentId, options = {}) => {
|
|
|
86
110
|
c = Number(t.completion ?? t.completion_tokens ?? 0);
|
|
87
111
|
k = Number(t.cached ?? t.cached_tokens ?? 0);
|
|
88
112
|
tot = Number(t.total ?? t.total_tokens ?? (p + c));
|
|
89
|
-
cost = Number(t.cost_usd ?? (
|
|
113
|
+
cost = Number(t.cost_usd ?? calculateCost(p, c, t.model || t.modelId || model));
|
|
90
114
|
} else {
|
|
91
|
-
const parsed = parseTranscriptFile(options.logPath || findTranscriptLog(options));
|
|
115
|
+
const parsed = parseTranscriptFile(options.logPath || findTranscriptLog(options), { model });
|
|
92
116
|
p = parsed.promptTokens; c = parsed.completionTokens; k = parsed.cachedTokens;
|
|
93
117
|
tot = parsed.totalTokens; cost = parsed.costUsd;
|
|
94
118
|
}
|
|
95
|
-
db.prepare(
|
|
119
|
+
db.prepare('UPDATE agent_tasks SET prompt_tokens=?, completion_tokens=?, cached_tokens=?, total_tokens=?, cost_usd=? WHERE id=?')
|
|
96
120
|
.run(p, c, k, tot, cost, Number(taskId));
|
|
97
121
|
const cleanId = normalizeAgentId(agentId);
|
|
98
122
|
if (cleanId) {
|
|
99
|
-
db.prepare(
|
|
123
|
+
db.prepare('UPDATE agents SET total_prompt_tokens=total_prompt_tokens+?, total_completion_tokens=total_completion_tokens+?, total_tokens=total_tokens+?, total_cost_usd=total_cost_usd+? WHERE id=?')
|
|
100
124
|
.run(p, c, tot, cost, cleanId);
|
|
101
125
|
}
|
|
102
126
|
return { prompt_tokens: p, completion_tokens: c, cached_tokens: k, total_tokens: tot, cost_usd: cost };
|
|
103
127
|
};
|
|
104
|
-
|
package/cli/theme.js
CHANGED