@illuminis/comprism 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +15 -0
- package/README.md +281 -0
- package/out/agent/command.d.ts +86 -0
- package/out/agent/command.js +259 -0
- package/out/agent/render.d.ts +97 -0
- package/out/agent/render.js +255 -0
- package/out/agent/session.d.ts +175 -0
- package/out/agent/session.js +573 -0
- package/out/commands/ask.d.ts +1 -0
- package/out/commands/ask.js +146 -0
- package/out/commands/codemap.d.ts +2 -0
- package/out/commands/codemap.js +151 -0
- package/out/commands/commands-thin.d.ts +39 -0
- package/out/commands/commands-thin.js +182 -0
- package/out/commands/install.d.ts +163 -0
- package/out/commands/install.js +543 -0
- package/out/commands/keys.d.ts +55 -0
- package/out/commands/keys.js +344 -0
- package/out/commands/login.d.ts +9 -0
- package/out/commands/login.js +384 -0
- package/out/commands/repl.d.ts +1 -0
- package/out/commands/repl.js +752 -0
- package/out/commands/settings.d.ts +21 -0
- package/out/commands/settings.js +244 -0
- package/out/commands/welcome.d.ts +1 -0
- package/out/commands/welcome.js +196 -0
- package/out/executor/documents.d.ts +40 -0
- package/out/executor/documents.js +170 -0
- package/out/executor/files.d.ts +2 -0
- package/out/executor/files.js +360 -0
- package/out/executor/git.d.ts +48 -0
- package/out/executor/git.js +132 -0
- package/out/executor/hooks.d.ts +67 -0
- package/out/executor/hooks.js +247 -0
- package/out/executor/index.d.ts +29 -0
- package/out/executor/index.js +221 -0
- package/out/executor/notebook.d.ts +2 -0
- package/out/executor/notebook.js +147 -0
- package/out/executor/paths.d.ts +15 -0
- package/out/executor/paths.js +126 -0
- package/out/executor/shell.d.ts +41 -0
- package/out/executor/shell.js +336 -0
- package/out/graph/build.d.ts +45 -0
- package/out/graph/build.js +91 -0
- package/out/graph/facts.d.ts +47 -0
- package/out/graph/facts.js +12 -0
- package/out/graph/files.d.ts +45 -0
- package/out/graph/files.js +207 -0
- package/out/graph/read-locales.d.ts +29 -0
- package/out/graph/read-locales.js +246 -0
- package/out/graph/read-python.d.ts +11 -0
- package/out/graph/read-python.js +115 -0
- package/out/graph/read-typescript.d.ts +16 -0
- package/out/graph/read-typescript.js +292 -0
- package/out/graph/sync.d.ts +66 -0
- package/out/graph/sync.js +242 -0
- package/out/lib/attach.d.ts +62 -0
- package/out/lib/attach.js +228 -0
- package/out/lib/config.d.ts +93 -0
- package/out/lib/config.js +198 -0
- package/out/lib/connection.d.ts +73 -0
- package/out/lib/connection.js +188 -0
- package/out/lib/gateway.d.ts +239 -0
- package/out/lib/gateway.js +171 -0
- package/out/lib/prompt.d.ts +34 -0
- package/out/lib/prompt.js +108 -0
- package/out/lib/types.d.ts +417 -0
- package/out/lib/types.js +21 -0
- package/out/lib/ui.d.ts +114 -0
- package/out/lib/ui.js +265 -0
- package/out/lib/version.d.ts +24 -0
- package/out/lib/version.js +27 -0
- package/out/lib/voice.d.ts +50 -0
- package/out/lib/voice.js +218 -0
- package/out/postinstall.d.ts +2 -0
- package/out/postinstall.js +92 -0
- package/out/thin.d.ts +2 -0
- package/out/thin.js +259 -0
- package/package.json +101 -0
- package/scripts/read_python.py +270 -0
package/out/thin.d.ts
ADDED
package/out/thin.js
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
4
|
+
if (k2 === undefined) k2 = k;
|
|
5
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
6
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
8
|
+
}
|
|
9
|
+
Object.defineProperty(o, k2, desc);
|
|
10
|
+
}) : (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
o[k2] = m[k];
|
|
13
|
+
}));
|
|
14
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
15
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
16
|
+
}) : function(o, v) {
|
|
17
|
+
o["default"] = v;
|
|
18
|
+
});
|
|
19
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
20
|
+
var ownKeys = function(o) {
|
|
21
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
22
|
+
var ar = [];
|
|
23
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
24
|
+
return ar;
|
|
25
|
+
};
|
|
26
|
+
return ownKeys(o);
|
|
27
|
+
};
|
|
28
|
+
return function (mod) {
|
|
29
|
+
if (mod && mod.__esModule) return mod;
|
|
30
|
+
var result = {};
|
|
31
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
32
|
+
__setModuleDefault(result, mod);
|
|
33
|
+
return result;
|
|
34
|
+
};
|
|
35
|
+
})();
|
|
36
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
|
+
/**
|
|
38
|
+
* The published command line tool.
|
|
39
|
+
*
|
|
40
|
+
* ── Why there are two entry points ────────────────────────────────────────
|
|
41
|
+
*
|
|
42
|
+
* `cli.ts` is the full internal tool. It reaches into the decision, the price
|
|
43
|
+
* list and the study, because our own work needs to. It is never published.
|
|
44
|
+
*
|
|
45
|
+
* THIS is what a customer installs. It carries no decision, no price list, no
|
|
46
|
+
* task representation and no study. Every question that needs any of those is
|
|
47
|
+
* asked of our server and only the answer comes back.
|
|
48
|
+
*
|
|
49
|
+
* The split is deliberate rather than tidy. A single entry point with the
|
|
50
|
+
* dangerous parts behind lazy imports still SHIPS them, and a package on a
|
|
51
|
+
* public registry is readable by anyone who installs it, forever. The only
|
|
52
|
+
* reliable way to not ship something is to not put it in the file that ships.
|
|
53
|
+
*
|
|
54
|
+
* The rule, from the owner: anything a client installs is thin; anything
|
|
55
|
+
* model-related runs on our server.
|
|
56
|
+
*/
|
|
57
|
+
const ui = __importStar(require("./lib/ui"));
|
|
58
|
+
const version_1 = require("./lib/version");
|
|
59
|
+
const out = (s = '') => process.stdout.write(s + '\n');
|
|
60
|
+
/**
|
|
61
|
+
* The value after a flag, or undefined when the flag was not given.
|
|
62
|
+
*
|
|
63
|
+
* `args[args.indexOf('--mode') + 1]` reads the FIRST argument when the flag is
|
|
64
|
+
* absent, because `indexOf` answers -1 and -1 + 1 is 0. So
|
|
65
|
+
* `comprism work "create a.txt"` took the request itself as the permission mode
|
|
66
|
+
* and refused the whole command before it started: the plainest way anybody
|
|
67
|
+
* runs this tool was the one way it could not be run.
|
|
68
|
+
*/
|
|
69
|
+
function flag(args, name) {
|
|
70
|
+
const at = args.indexOf(name);
|
|
71
|
+
return at === -1 ? undefined : args[at + 1];
|
|
72
|
+
}
|
|
73
|
+
function usage() {
|
|
74
|
+
out(ui.banner('CompletionPrism'));
|
|
75
|
+
out(ui.c.dim(' Every answer on the model that finishes it for least, with a receipt.'));
|
|
76
|
+
out();
|
|
77
|
+
out(ui.table(['COMMAND', 'WHAT IT DOES'], [
|
|
78
|
+
['comprism ask "<your question>"', 'one prompt, one answer, and what it cost'],
|
|
79
|
+
['comprism work', 'have it do the work: reads your project, edits, runs your tests'],
|
|
80
|
+
['comprism login', 'sign in with your email and password'],
|
|
81
|
+
['comprism', 'start a session and just type your questions'],
|
|
82
|
+
['comprism keys', 'add a provider key, or point at a file of them'],
|
|
83
|
+
['comprism model', 'the model your savings are measured against'],
|
|
84
|
+
['comprism providers', 'every AI vendor and model, and what you can reach'],
|
|
85
|
+
['comprism workspace', 'the folder to work in, default is where you are'],
|
|
86
|
+
['comprism status', 'is this machine signed in, and to what'],
|
|
87
|
+
['comprism check', 'what this machine still needs, and how to add it'],
|
|
88
|
+
['comprism logout', 'sign out, and revoke this machine\'s key'],
|
|
89
|
+
]));
|
|
90
|
+
out();
|
|
91
|
+
out(ui.c.dim(` v${version_1.COMPRISM_VERSION} · every decision is made on our servers`));
|
|
92
|
+
out();
|
|
93
|
+
}
|
|
94
|
+
async function main() {
|
|
95
|
+
const [, , command, ...args] = process.argv;
|
|
96
|
+
switch (command) {
|
|
97
|
+
case 'ask': {
|
|
98
|
+
const { cmdAsk } = await Promise.resolve().then(() => __importStar(require('./commands/ask')));
|
|
99
|
+
return cmdAsk(args);
|
|
100
|
+
}
|
|
101
|
+
// `work` is the word. `agent` still answers to it, because anybody who
|
|
102
|
+
// learned the old name should not meet "unknown command".
|
|
103
|
+
case 'work':
|
|
104
|
+
case 'agent': {
|
|
105
|
+
const { runAgent, AGENT_HELP } = await Promise.resolve().then(() => __importStar(require('./agent/command')));
|
|
106
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
107
|
+
out(AGENT_HELP);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const { subject, closePrompt } = await Promise.resolve().then(() => __importStar(require('./lib/prompt')));
|
|
111
|
+
// Asked for when it was not given, same as every other command.
|
|
112
|
+
const request = await subject(args, ' what would you like done? ', ['--mode', '--model', '--max-spend', '--resume', '--cwd', '--allow']);
|
|
113
|
+
closePrompt();
|
|
114
|
+
if (!request) {
|
|
115
|
+
out(ui.fail('Nothing was asked for.'));
|
|
116
|
+
out();
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const spend = flag(args, '--max-spend');
|
|
120
|
+
process.exitCode = await runAgent({
|
|
121
|
+
request: [request],
|
|
122
|
+
mode: flag(args, '--mode'),
|
|
123
|
+
plan: args.includes('--plan'),
|
|
124
|
+
approvePlan: args.includes('--approve-plan'),
|
|
125
|
+
model: flag(args, '--model'),
|
|
126
|
+
pin: args.includes('--pin'),
|
|
127
|
+
...(args.includes('--max-spend') ? { maxSpend: Number(spend) } : {}),
|
|
128
|
+
allow: args.reduce((acc, a, i) => (args[i - 1] === '--allow' ? [...acc, a] : acc), []),
|
|
129
|
+
yes: args.includes('--yes'),
|
|
130
|
+
background: args.includes('--background'),
|
|
131
|
+
resume: flag(args, '--resume'),
|
|
132
|
+
// Go without the code map for this one job. Two reasons somebody uses
|
|
133
|
+
// it: they are measuring, and a run with a map and a run without one
|
|
134
|
+
// are two different quantities; or they suspect the map and want the
|
|
135
|
+
// files read directly. Both are legitimate and neither should require
|
|
136
|
+
// an administrator.
|
|
137
|
+
noMap: args.includes('--no-map') || args.includes('--no-graph'),
|
|
138
|
+
// The saved working folder when there is one, so `comprism workspace`
|
|
139
|
+
// is a setting the agent honors rather than a stored string nothing reads.
|
|
140
|
+
cwd: flag(args, '--cwd')
|
|
141
|
+
?? (await Promise.resolve().then(() => __importStar(require('./commands/settings')))).currentWorkspace().path,
|
|
142
|
+
});
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
// The map of the project you are standing in: what it holds, and a way to
|
|
146
|
+
// have it read again. `graph` answers to it as well, because that is what
|
|
147
|
+
// the thing is called in the documentation and in the patent.
|
|
148
|
+
case 'map':
|
|
149
|
+
case 'graph': {
|
|
150
|
+
const { cmdMap } = await Promise.resolve().then(() => __importStar(require('./commands/codemap')));
|
|
151
|
+
process.exitCode = await cmdMap(args);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
case 'login': {
|
|
155
|
+
const { cmdLogin } = await Promise.resolve().then(() => __importStar(require('./commands/login')));
|
|
156
|
+
await cmdLogin(args);
|
|
157
|
+
// Signing in is not the thing somebody came to do. It used to print the
|
|
158
|
+
// header and exit, so a person who had just signed in had to type the
|
|
159
|
+
// program's name a second time to get to a prompt. If the sign in
|
|
160
|
+
// worked and there is somebody at the keyboard, carry straight on into
|
|
161
|
+
// the session.
|
|
162
|
+
//
|
|
163
|
+
// Only with a terminal attached: a script that signs in must not be
|
|
164
|
+
// handed an interactive prompt and left waiting for a line that never
|
|
165
|
+
// comes. And only when it actually worked, which the exit code says.
|
|
166
|
+
if (!process.exitCode && process.stdin.isTTY) {
|
|
167
|
+
const { runRepl } = await Promise.resolve().then(() => __importStar(require('./commands/repl')));
|
|
168
|
+
return runRepl();
|
|
169
|
+
}
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
// Kept as a second name for the same job. Anybody who learned the old word
|
|
173
|
+
// should not hit "unknown command" for it.
|
|
174
|
+
case 'connect': {
|
|
175
|
+
const { cmdLogin } = await Promise.resolve().then(() => __importStar(require('./commands/login')));
|
|
176
|
+
return cmdLogin(args);
|
|
177
|
+
}
|
|
178
|
+
// The three words people type for the same act. `logout` used to answer
|
|
179
|
+
// "there is no command called logout", which is the product telling
|
|
180
|
+
// somebody their own vocabulary is wrong.
|
|
181
|
+
case 'logout':
|
|
182
|
+
case 'signout':
|
|
183
|
+
case 'disconnect': {
|
|
184
|
+
const { cmdDisconnect } = await Promise.resolve().then(() => __importStar(require('./commands/commands-thin')));
|
|
185
|
+
return cmdDisconnect();
|
|
186
|
+
}
|
|
187
|
+
case 'model': {
|
|
188
|
+
const { cmdModel } = await Promise.resolve().then(() => __importStar(require('./commands/settings')));
|
|
189
|
+
return cmdModel(args);
|
|
190
|
+
}
|
|
191
|
+
case 'providers': {
|
|
192
|
+
const { cmdProviders } = await Promise.resolve().then(() => __importStar(require('./commands/settings')));
|
|
193
|
+
return cmdProviders();
|
|
194
|
+
}
|
|
195
|
+
case 'workspace': {
|
|
196
|
+
const { cmdWorkspace } = await Promise.resolve().then(() => __importStar(require('./commands/settings')));
|
|
197
|
+
return cmdWorkspace(args);
|
|
198
|
+
}
|
|
199
|
+
case 'keys': {
|
|
200
|
+
const { cmdKeys } = await Promise.resolve().then(() => __importStar(require('./commands/keys')));
|
|
201
|
+
return cmdKeys(args);
|
|
202
|
+
}
|
|
203
|
+
case 'status': {
|
|
204
|
+
const { cmdStatus } = await Promise.resolve().then(() => __importStar(require('./commands/commands-thin')));
|
|
205
|
+
return cmdStatus();
|
|
206
|
+
}
|
|
207
|
+
// What this machine still needs. Its own word because somebody whose PDF
|
|
208
|
+
// would not read has a question about THIS machine, and the answer should
|
|
209
|
+
// not be buried inside a setup command they have already run.
|
|
210
|
+
case 'check':
|
|
211
|
+
case 'doctor': {
|
|
212
|
+
const { check } = await Promise.resolve().then(() => __importStar(require('./lib/readiness')));
|
|
213
|
+
const machine = check();
|
|
214
|
+
out();
|
|
215
|
+
out(ui.banner('This machine'));
|
|
216
|
+
out();
|
|
217
|
+
for (const line of machine.lines)
|
|
218
|
+
out(line);
|
|
219
|
+
out();
|
|
220
|
+
if (machine.ready) {
|
|
221
|
+
out(ui.ok('Everything the work needs is here.'));
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
out(` ${ui.c.dim('to add what is missing:')} ${ui.c.bold(machine.fix)}`);
|
|
225
|
+
}
|
|
226
|
+
out();
|
|
227
|
+
out(ui.c.dim(' Word, PowerPoint and PDF documents are built on our servers,'));
|
|
228
|
+
out(ui.c.dim(' so nothing has to be installed here for those.'));
|
|
229
|
+
out();
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
case '--version':
|
|
233
|
+
case '-v':
|
|
234
|
+
out(version_1.COMPRISM_VERSION);
|
|
235
|
+
return;
|
|
236
|
+
case 'help':
|
|
237
|
+
case '--help':
|
|
238
|
+
case '-h':
|
|
239
|
+
return usage();
|
|
240
|
+
default: {
|
|
241
|
+
// The bare command shows WHERE YOU ARE, not a list of other commands. A
|
|
242
|
+
// person who does not know which model their savings are measured
|
|
243
|
+
// against cannot read a saving, and they will not run three commands to
|
|
244
|
+
// find out. The command list is one word away, under `help`.
|
|
245
|
+
if (command) {
|
|
246
|
+
out(ui.fail(`There is no command called "${command}".`));
|
|
247
|
+
out();
|
|
248
|
+
return usage();
|
|
249
|
+
}
|
|
250
|
+
// The bare command STARTS the product rather than describing it.
|
|
251
|
+
const { runRepl } = await Promise.resolve().then(() => __importStar(require('./commands/repl')));
|
|
252
|
+
return runRepl();
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
main().catch((err) => {
|
|
257
|
+
process.stderr.write(ui.fail(err instanceof Error ? err.message : 'something went wrong') + '\n');
|
|
258
|
+
process.exitCode = 1;
|
|
259
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@illuminis/comprism",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CompletionPrism Companion. Records what finishing a piece of work actually costs - every attempt, and the human time spent noticing and repairing the ones that failed.",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"private": false,
|
|
7
|
+
"main": "out/thin.js",
|
|
8
|
+
"types": "out/thin.d.ts",
|
|
9
|
+
"bin": {
|
|
10
|
+
"comprism": "out/thin.js"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"out/thin.js",
|
|
14
|
+
"out/thin.d.ts",
|
|
15
|
+
"out/postinstall.js",
|
|
16
|
+
"out/postinstall.d.ts",
|
|
17
|
+
"out/commands/install.js",
|
|
18
|
+
"out/commands/install.d.ts",
|
|
19
|
+
"out/lib/config.js",
|
|
20
|
+
"out/lib/config.d.ts",
|
|
21
|
+
"out/lib/connection.js",
|
|
22
|
+
"out/lib/connection.d.ts",
|
|
23
|
+
"out/lib/gateway.js",
|
|
24
|
+
"out/lib/gateway.d.ts",
|
|
25
|
+
"out/lib/prompt.js",
|
|
26
|
+
"out/lib/prompt.d.ts",
|
|
27
|
+
"out/lib/types.js",
|
|
28
|
+
"out/lib/types.d.ts",
|
|
29
|
+
"out/lib/ui.js",
|
|
30
|
+
"out/lib/ui.d.ts",
|
|
31
|
+
"out/lib/version.js",
|
|
32
|
+
"out/lib/version.d.ts",
|
|
33
|
+
"out/lib/attach.js",
|
|
34
|
+
"out/lib/attach.d.ts",
|
|
35
|
+
"out/lib/voice.js",
|
|
36
|
+
"out/lib/voice.d.ts",
|
|
37
|
+
"out/commands/ask.js",
|
|
38
|
+
"out/commands/ask.d.ts",
|
|
39
|
+
"out/commands/commands-thin.js",
|
|
40
|
+
"out/commands/commands-thin.d.ts",
|
|
41
|
+
"out/commands/keys.js",
|
|
42
|
+
"out/commands/keys.d.ts",
|
|
43
|
+
"out/commands/login.js",
|
|
44
|
+
"out/commands/login.d.ts",
|
|
45
|
+
"out/commands/repl.js",
|
|
46
|
+
"out/commands/repl.d.ts",
|
|
47
|
+
"out/commands/settings.js",
|
|
48
|
+
"out/commands/settings.d.ts",
|
|
49
|
+
"out/commands/welcome.js",
|
|
50
|
+
"out/commands/welcome.d.ts",
|
|
51
|
+
"out/graph/**",
|
|
52
|
+
"out/commands/codemap.js",
|
|
53
|
+
"out/commands/codemap.d.ts",
|
|
54
|
+
"scripts/read_python.py",
|
|
55
|
+
"out/agent/**",
|
|
56
|
+
"out/executor/**",
|
|
57
|
+
"!out/internal/**",
|
|
58
|
+
"README.md",
|
|
59
|
+
"LICENSE"
|
|
60
|
+
],
|
|
61
|
+
"engines": {
|
|
62
|
+
"node": ">=18"
|
|
63
|
+
},
|
|
64
|
+
"scripts": {
|
|
65
|
+
"build": "tsc -p ./",
|
|
66
|
+
"dev": "tsc -w -p ./",
|
|
67
|
+
"test": "npm run build && node --test out/**/*.test.js",
|
|
68
|
+
"gate": "npm run build && npm test && npm run shipcheck",
|
|
69
|
+
"prepublishOnly": "npm run build",
|
|
70
|
+
"prepack": "npm run build",
|
|
71
|
+
"postinstall": "node out/postinstall.js || true",
|
|
72
|
+
"shipcheck": "node scripts/check-no-model-logic.mjs"
|
|
73
|
+
},
|
|
74
|
+
"devDependencies": {
|
|
75
|
+
"@types/node": "^20.11.0"
|
|
76
|
+
},
|
|
77
|
+
"publishConfig": {
|
|
78
|
+
"access": "public",
|
|
79
|
+
"registry": "https://registry.npmjs.org/"
|
|
80
|
+
},
|
|
81
|
+
"repository": {
|
|
82
|
+
"type": "git",
|
|
83
|
+
"url": "git+https://github.com/Illuminis-ai/completionprism.git",
|
|
84
|
+
"directory": "app/cli"
|
|
85
|
+
},
|
|
86
|
+
"keywords": [
|
|
87
|
+
"ai",
|
|
88
|
+
"cost",
|
|
89
|
+
"observability",
|
|
90
|
+
"llm",
|
|
91
|
+
"completion"
|
|
92
|
+
],
|
|
93
|
+
"author": "illuminis.ai LLC",
|
|
94
|
+
"homepage": "https://completionprism.illuminis.ai",
|
|
95
|
+
"bugs": {
|
|
96
|
+
"url": "https://github.com/Illuminis-ai/completionprism/issues"
|
|
97
|
+
},
|
|
98
|
+
"dependencies": {
|
|
99
|
+
"typescript": "^5.4.0"
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Read Python files into code map facts, using Python's own parser.
|
|
3
|
+
|
|
4
|
+
This is a parser, not a model. Nothing here guesses: every object and every
|
|
5
|
+
link comes from the syntax tree, so the same file always produces the same
|
|
6
|
+
facts and two machines reading one project agree.
|
|
7
|
+
|
|
8
|
+
It runs on the person's own machine and prints facts, never source. What
|
|
9
|
+
leaves is "a function called price_of exists at line 40 of this file, and
|
|
10
|
+
these things call it". What it says is never read by anything but the parser.
|
|
11
|
+
|
|
12
|
+
Usage: read_python.py <project-root> <relative-path-list-file>
|
|
13
|
+
Writes one JSON object to stdout.
|
|
14
|
+
"""
|
|
15
|
+
import ast
|
|
16
|
+
import json
|
|
17
|
+
import os
|
|
18
|
+
import sys
|
|
19
|
+
|
|
20
|
+
ROUTE_METHODS = {"get", "post", "put", "patch", "delete", "head", "options"}
|
|
21
|
+
MODEL_BASE_HINTS = {"Base", "BaseDataMixin", "DeclarativeBase", "Model",
|
|
22
|
+
"SQLModel", "models.Model"}
|
|
23
|
+
COLUMN_FACTORIES = {"Column", "mapped_column", "relationship"}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def dotted(node):
|
|
27
|
+
"""Render a Name / Attribute chain back to its source text."""
|
|
28
|
+
if isinstance(node, ast.Name):
|
|
29
|
+
return node.id
|
|
30
|
+
if isinstance(node, ast.Attribute):
|
|
31
|
+
base = dotted(node.value)
|
|
32
|
+
return "%s.%s" % (base, node.attr) if base else node.attr
|
|
33
|
+
if isinstance(node, ast.Call):
|
|
34
|
+
return dotted(node.func)
|
|
35
|
+
if isinstance(node, ast.Subscript):
|
|
36
|
+
return dotted(node.value)
|
|
37
|
+
return ""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def literal_str(node):
|
|
41
|
+
if isinstance(node, ast.Constant) and isinstance(node.value, str):
|
|
42
|
+
return node.value
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class ModuleReader(ast.NodeVisitor):
|
|
47
|
+
def __init__(self, file_id, module_path):
|
|
48
|
+
self.file_id = file_id
|
|
49
|
+
self.module_path = module_path
|
|
50
|
+
self.objects = []
|
|
51
|
+
self.links = []
|
|
52
|
+
self.scope = []
|
|
53
|
+
self.owner = [file_id]
|
|
54
|
+
self.router_prefixes = {}
|
|
55
|
+
self.mounts = []
|
|
56
|
+
|
|
57
|
+
def qual(self, name):
|
|
58
|
+
return ".".join(self.scope + [name])
|
|
59
|
+
|
|
60
|
+
def add_object(self, object_id, kind, name, line, **extra):
|
|
61
|
+
entry = {"id": object_id, "kind": kind, "name": name,
|
|
62
|
+
"file": self.file_id, "line": line}
|
|
63
|
+
entry.update({k: v for k, v in extra.items() if v not in (None, [], "")})
|
|
64
|
+
self.objects.append(entry)
|
|
65
|
+
|
|
66
|
+
def add_link(self, src, kind, dst=None, to_name=None, **extra):
|
|
67
|
+
link = {"from": src, "kind": kind}
|
|
68
|
+
if dst:
|
|
69
|
+
link["to"] = dst
|
|
70
|
+
if to_name:
|
|
71
|
+
link["to_name"] = to_name
|
|
72
|
+
link.update(extra)
|
|
73
|
+
self.links.append(link)
|
|
74
|
+
|
|
75
|
+
# ---- imports ---------------------------------------------------------
|
|
76
|
+
def visit_Import(self, node):
|
|
77
|
+
for alias in node.names:
|
|
78
|
+
self.add_link(self.file_id, "imports", to_name=alias.name, line=node.lineno)
|
|
79
|
+
self.generic_visit(node)
|
|
80
|
+
|
|
81
|
+
def visit_ImportFrom(self, node):
|
|
82
|
+
module = node.module or ""
|
|
83
|
+
if node.level:
|
|
84
|
+
base = self.module_path.rsplit(".", node.level)[0]
|
|
85
|
+
module = "%s.%s" % (base, module) if module else base
|
|
86
|
+
for alias in node.names:
|
|
87
|
+
self.add_link(
|
|
88
|
+
self.file_id, "imports",
|
|
89
|
+
to_name=("%s.%s" % (module, alias.name)) if module else alias.name,
|
|
90
|
+
line=node.lineno)
|
|
91
|
+
self.generic_visit(node)
|
|
92
|
+
|
|
93
|
+
# ---- routers ---------------------------------------------------------
|
|
94
|
+
def visit_Assign(self, node):
|
|
95
|
+
"""`router = APIRouter(prefix="/billing")` fixes every path below it."""
|
|
96
|
+
if not self.scope and isinstance(node.value, ast.Call):
|
|
97
|
+
leaf = dotted(node.value.func).split(".")[-1]
|
|
98
|
+
if leaf in ("APIRouter", "Blueprint", "Router"):
|
|
99
|
+
prefix = ""
|
|
100
|
+
for kw in node.value.keywords:
|
|
101
|
+
if kw.arg in ("prefix", "url_prefix"):
|
|
102
|
+
prefix = literal_str(kw.value) or ""
|
|
103
|
+
for target in node.targets:
|
|
104
|
+
if isinstance(target, ast.Name):
|
|
105
|
+
self.router_prefixes[target.id] = prefix
|
|
106
|
+
self.generic_visit(node)
|
|
107
|
+
|
|
108
|
+
def _include_router(self, node):
|
|
109
|
+
"""`include_router(billing.router, prefix="/api")` mounts a whole file."""
|
|
110
|
+
prefix = ""
|
|
111
|
+
for kw in node.keywords:
|
|
112
|
+
if kw.arg in ("prefix", "url_prefix"):
|
|
113
|
+
prefix = literal_str(kw.value) or ""
|
|
114
|
+
if not node.args:
|
|
115
|
+
return
|
|
116
|
+
target = dotted(node.args[0])
|
|
117
|
+
if target:
|
|
118
|
+
self.mounts.append({"target": target, "prefix": prefix, "line": node.lineno})
|
|
119
|
+
|
|
120
|
+
# ---- classes ---------------------------------------------------------
|
|
121
|
+
def visit_ClassDef(self, node):
|
|
122
|
+
qual = self.qual(node.name)
|
|
123
|
+
object_id = "%s::%s" % (self.file_id, qual)
|
|
124
|
+
bases = [dotted(b) for b in node.bases]
|
|
125
|
+
tablename = None
|
|
126
|
+
columns = []
|
|
127
|
+
for stmt in node.body:
|
|
128
|
+
if isinstance(stmt, ast.Assign):
|
|
129
|
+
for target in stmt.targets:
|
|
130
|
+
if isinstance(target, ast.Name):
|
|
131
|
+
if target.id == "__tablename__":
|
|
132
|
+
tablename = literal_str(stmt.value)
|
|
133
|
+
elif isinstance(stmt.value, ast.Call) and \
|
|
134
|
+
dotted(stmt.value.func).split(".")[-1] in COLUMN_FACTORIES:
|
|
135
|
+
columns.append(target.id)
|
|
136
|
+
elif isinstance(stmt, ast.AnnAssign) and isinstance(stmt.target, ast.Name):
|
|
137
|
+
if isinstance(stmt.value, ast.Call) and \
|
|
138
|
+
dotted(stmt.value.func).split(".")[-1] in COLUMN_FACTORIES:
|
|
139
|
+
columns.append(stmt.target.id)
|
|
140
|
+
|
|
141
|
+
is_model = bool(tablename) or any(
|
|
142
|
+
b.split(".")[-1] in MODEL_BASE_HINTS for b in bases)
|
|
143
|
+
self.add_object(object_id, "model" if is_model else "class", qual,
|
|
144
|
+
node.lineno, bases=bases, table=tablename, columns=columns)
|
|
145
|
+
self.add_link(self.file_id, "defines", object_id)
|
|
146
|
+
for base in bases:
|
|
147
|
+
self.add_link(object_id, "inherits", to_name=base)
|
|
148
|
+
if tablename:
|
|
149
|
+
table_id = "table:%s" % tablename
|
|
150
|
+
self.add_object(table_id, "table", tablename, node.lineno)
|
|
151
|
+
self.add_link(object_id, "maps_to", table_id)
|
|
152
|
+
|
|
153
|
+
self.scope.append(node.name)
|
|
154
|
+
self.owner.append(object_id)
|
|
155
|
+
self.generic_visit(node)
|
|
156
|
+
self.owner.pop()
|
|
157
|
+
self.scope.pop()
|
|
158
|
+
|
|
159
|
+
# ---- functions -------------------------------------------------------
|
|
160
|
+
def _function(self, node, is_async):
|
|
161
|
+
qual = self.qual(node.name)
|
|
162
|
+
object_id = "%s::%s" % (self.file_id, qual)
|
|
163
|
+
decorators = [dotted(d) for d in node.decorator_list]
|
|
164
|
+
self.add_object(object_id, "method" if self.scope else "function", qual,
|
|
165
|
+
node.lineno, decorators=decorators,
|
|
166
|
+
is_async=is_async or None,
|
|
167
|
+
args=[a.arg for a in node.args.args])
|
|
168
|
+
self.add_link(self.file_id, "defines", object_id)
|
|
169
|
+
|
|
170
|
+
# An endpoint: @router.get("/things"). The path here is RELATIVE to its
|
|
171
|
+
# own router; the service puts the two halves back together.
|
|
172
|
+
for dec in node.decorator_list:
|
|
173
|
+
if not isinstance(dec, ast.Call):
|
|
174
|
+
continue
|
|
175
|
+
target = dotted(dec.func)
|
|
176
|
+
verb = target.split(".")[-1].lower()
|
|
177
|
+
if verb not in ROUTE_METHODS:
|
|
178
|
+
continue
|
|
179
|
+
path = literal_str(dec.args[0]) if dec.args else None
|
|
180
|
+
if path is None:
|
|
181
|
+
continue
|
|
182
|
+
router_var = target.rsplit(".", 1)[0] if "." in target else ""
|
|
183
|
+
route_id = "route:%s %s@%s" % (verb.upper(), path, self.file_id)
|
|
184
|
+
self.add_object(route_id, "route", "%s %s" % (verb.upper(), path),
|
|
185
|
+
node.lineno, method=verb.upper(), path=path,
|
|
186
|
+
router_var=router_var)
|
|
187
|
+
self.add_link(route_id, "handled_by", object_id)
|
|
188
|
+
self.add_link(self.file_id, "declares", route_id)
|
|
189
|
+
|
|
190
|
+
self.scope.append(node.name)
|
|
191
|
+
self.owner.append(object_id)
|
|
192
|
+
self.generic_visit(node)
|
|
193
|
+
self.owner.pop()
|
|
194
|
+
self.scope.pop()
|
|
195
|
+
|
|
196
|
+
def visit_FunctionDef(self, node):
|
|
197
|
+
self._function(node, False)
|
|
198
|
+
|
|
199
|
+
def visit_AsyncFunctionDef(self, node):
|
|
200
|
+
self._function(node, True)
|
|
201
|
+
|
|
202
|
+
def visit_Name(self, node):
|
|
203
|
+
"""A capitalized name used in an expression is nearly always a class or
|
|
204
|
+
a model being referenced, which is how database usage shows up."""
|
|
205
|
+
if isinstance(node.ctx, ast.Load) and node.id[:1].isupper():
|
|
206
|
+
self.add_link(self.owner[-1], "references", to_name=node.id, line=node.lineno)
|
|
207
|
+
self.generic_visit(node)
|
|
208
|
+
|
|
209
|
+
def visit_Call(self, node):
|
|
210
|
+
target = dotted(node.func)
|
|
211
|
+
if target.split(".")[-1] in ("include_router", "register_blueprint"):
|
|
212
|
+
self._include_router(node)
|
|
213
|
+
if target:
|
|
214
|
+
leaf = target.split(".")[-1]
|
|
215
|
+
if leaf and not leaf.startswith("_"):
|
|
216
|
+
self.add_link(self.owner[-1], "calls", to_name=leaf,
|
|
217
|
+
full_name=target, line=node.lineno)
|
|
218
|
+
self.generic_visit(node)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def module_path_for(rel_path):
|
|
222
|
+
trimmed = rel_path[:-3]
|
|
223
|
+
if trimmed.endswith("/__init__"):
|
|
224
|
+
trimmed = trimmed[: -len("/__init__")]
|
|
225
|
+
return trimmed.replace("/", ".")
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def main():
|
|
229
|
+
if len(sys.argv) < 3:
|
|
230
|
+
sys.stderr.write("usage: read_python.py <project-root> <path-list-file>\n")
|
|
231
|
+
return 2
|
|
232
|
+
root = os.path.abspath(sys.argv[1])
|
|
233
|
+
with open(sys.argv[2], "r", encoding="utf8") as handle:
|
|
234
|
+
rels = [line.strip() for line in handle if line.strip()]
|
|
235
|
+
|
|
236
|
+
facts = {}
|
|
237
|
+
failures = []
|
|
238
|
+
for rel_path in rels:
|
|
239
|
+
full = os.path.join(root, rel_path)
|
|
240
|
+
try:
|
|
241
|
+
with open(full, "r", encoding="utf-8") as handle:
|
|
242
|
+
source = handle.read()
|
|
243
|
+
tree = ast.parse(source, filename=rel_path)
|
|
244
|
+
except (SyntaxError, UnicodeDecodeError, OSError, ValueError) as exc:
|
|
245
|
+
failures.append({"file": rel_path, "reason": type(exc).__name__})
|
|
246
|
+
continue
|
|
247
|
+
|
|
248
|
+
reader = ModuleReader(rel_path, module_path_for(rel_path))
|
|
249
|
+
reader.visit(tree)
|
|
250
|
+
entry = {
|
|
251
|
+
"objects": [{
|
|
252
|
+
"id": rel_path, "kind": "file",
|
|
253
|
+
"name": os.path.basename(rel_path), "file": rel_path, "line": 1,
|
|
254
|
+
"lang": "python", "lines": source.count("\n") + 1,
|
|
255
|
+
"module": module_path_for(rel_path),
|
|
256
|
+
}] + reader.objects,
|
|
257
|
+
"links": reader.links,
|
|
258
|
+
}
|
|
259
|
+
if reader.router_prefixes or reader.mounts:
|
|
260
|
+
entry["routers"] = {"prefixes": reader.router_prefixes,
|
|
261
|
+
"mounts": reader.mounts}
|
|
262
|
+
facts[rel_path] = entry
|
|
263
|
+
|
|
264
|
+
json.dump({"facts": facts, "failures": failures}, sys.stdout,
|
|
265
|
+
separators=(",", ":"))
|
|
266
|
+
return 0
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
if __name__ == "__main__":
|
|
270
|
+
sys.exit(main())
|