openmerit 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +12 -25
  3. package/dist/pi/scout-extension.mjs +20 -0
  4. package/dist/terminal/scout-update.json +1 -1
  5. package/dist/terminal/src/browser.mjs +2 -4
  6. package/dist/terminal/src/cli.mjs +4 -56
  7. package/dist/terminal/src/install.mjs +2 -2
  8. package/dist/terminal/src/scout-instructions.js +20 -0
  9. package/dist/terminal/src/scout-pi.js +1 -1
  10. package/dist/terminal/src/scout.js +35 -9
  11. package/dist/terminal/src/telemetry.mjs +8 -60
  12. package/docs/agent-evaluations.md +8 -148
  13. package/docs/commands.md +3 -9
  14. package/docs/getting-started.md +2 -16
  15. package/docs/index.md +4 -18
  16. package/docs/inference-terminal.md +13 -594
  17. package/docs/roadmap.md +2 -2
  18. package/docs/scout-data-policy.md +2 -1
  19. package/docs/scout.md +21 -2
  20. package/docs/security.md +5 -18
  21. package/docs/testing.md +4 -16
  22. package/docs/troubleshooting.md +2 -2
  23. package/package.json +10 -23
  24. package/dist/eval/src/adapters.mjs +0 -112
  25. package/dist/eval/src/cli.mjs +0 -151
  26. package/dist/eval/src/config.mjs +0 -108
  27. package/dist/eval/src/process.mjs +0 -44
  28. package/dist/eval/src/report.mjs +0 -59
  29. package/dist/eval/src/runner.mjs +0 -123
  30. package/dist/eval/src/scout.mjs +0 -48
  31. package/dist/terminal/public/app.js +0 -298
  32. package/dist/terminal/public/brands/anthropic.png +0 -0
  33. package/dist/terminal/public/brands/baai.png +0 -0
  34. package/dist/terminal/public/brands/baseten.png +0 -0
  35. package/dist/terminal/public/brands/cerebras.png +0 -0
  36. package/dist/terminal/public/brands/cohere.png +0 -0
  37. package/dist/terminal/public/brands/deepseek.ico +0 -0
  38. package/dist/terminal/public/brands/google.png +0 -0
  39. package/dist/terminal/public/brands/groq.ico +0 -0
  40. package/dist/terminal/public/brands/huggingface.svg +0 -8
  41. package/dist/terminal/public/brands/lm-studio.png +0 -0
  42. package/dist/terminal/public/brands/meta.ico +0 -0
  43. package/dist/terminal/public/brands/mistral.png +0 -0
  44. package/dist/terminal/public/brands/nomic.png +0 -0
  45. package/dist/terminal/public/brands/ollama.png +0 -0
  46. package/dist/terminal/public/brands/openai.png +0 -0
  47. package/dist/terminal/public/brands/openrouter.png +0 -0
  48. package/dist/terminal/public/brands/qwen.png +0 -0
  49. package/dist/terminal/public/brands/vllm.png +0 -0
  50. package/dist/terminal/public/favicon.svg +0 -1
  51. package/dist/terminal/public/index.html +0 -22
  52. package/dist/terminal/public/styles.css +0 -921
  53. package/dist/terminal/src/activity-merge.mjs +0 -66
  54. package/dist/terminal/src/collect.mjs +0 -370
  55. package/dist/terminal/src/coverage.mjs +0 -33
  56. package/dist/terminal/src/discovery.mjs +0 -99
  57. package/dist/terminal/src/hardware.mjs +0 -57
  58. package/dist/terminal/src/history.mjs +0 -32
  59. package/dist/terminal/src/log-continuity.mjs +0 -26
  60. package/dist/terminal/src/project-activity.mjs +0 -157
  61. package/dist/terminal/src/repository-background.mjs +0 -37
  62. package/dist/terminal/src/repository-index.mjs +0 -267
  63. package/dist/terminal/src/repository-policy.mjs +0 -3
  64. package/dist/terminal/src/repository-syntax.mjs +0 -236
  65. package/dist/terminal/src/repository-worker.mjs +0 -15
  66. package/dist/terminal/src/sample.mjs +0 -171
  67. package/dist/terminal/src/scout-credentials.d.ts +0 -12
  68. package/dist/terminal/src/scout-credentials.ts +0 -44
  69. package/dist/terminal/src/scout-instructions.d.ts +0 -5
  70. package/dist/terminal/src/scout-instructions.ts +0 -119
  71. package/dist/terminal/src/scout-pi.d.ts +0 -44
  72. package/dist/terminal/src/scout-pi.ts +0 -76
  73. package/dist/terminal/src/scout.d.ts +0 -77
  74. package/dist/terminal/src/scout.ts +0 -164
  75. package/dist/terminal/src/server.mjs +0 -95
  76. package/dist/terminal/src/service-registry.mjs +0 -78
  77. package/dist/terminal/src/services.mjs +0 -44
  78. package/dist/terminal/src/topology.mjs +0 -30
  79. package/dist/terminal/src/updates.d.ts +0 -62
  80. package/dist/terminal/src/updates.ts +0 -313
@@ -1,236 +0,0 @@
1
- // Parse syntax only. Never import, evaluate, source, or interpolate project code.
2
- import { parser as python } from '@lezer/python';
3
- import { parser as javascript } from '@lezer/javascript';
4
- import { parseDocument, isMap, isSeq, isScalar, isAlias, LineCounter } from 'yaml';
5
- import TOML from '@iarna/toml';
6
-
7
- const identifier = /^[A-Za-z_$][\w$]*$/;
8
- export const endpointField = key => /^(?:base_?url|api_?base|endpoint(?:_?url)?|(?:[A-Z][A-Z0-9_]*_)?(?:BASE_URL|API_BASE|ENDPOINT(?:_URL)?)(?:_\d+)?|OPENMERIT_LOCAL_URL|OLLAMA_HOST)$/i.test(key);
9
- export const modelField = key => /^(?:(?:[\w]+_)?models?(?:_ids?|_names?)?|modelName|modelId|model_name_or_path|pretrained_model_name_or_path)$/i.test(key) && !/^(?:model_creator|model_provider|model_type|model_path|model_dir|model_cache|model_kwargs)$/i.test(key);
10
- const taskField = key => /^(?:task(?:_id|_name)?|taskName|taskId)$/i.test(key);
11
- const candidateField = key => /(?:candidate|benchmark|leaderboard|strong_model|weak_model|models$)/i.test(key);
12
- const sensitive = key => /(?:key|token|secret|password|credential|prompt|message|content|response|header|payload|input|output)/i.test(key);
13
- const children = node => { const out=[]; for(let c=node.firstChild;c;c=c.nextSibling) out.push(c); return out; };
14
- const nodeText = (node, body) => node ? body.slice(node.from,node.to) : '';
15
- const lineAt = (body, offset) => body.slice(0,offset).split('\n').length;
16
- const literal = raw => {
17
- // Leave escapes, interpolated strings and multiline text unresolved. IDs and
18
- // endpoint literals do not need general-purpose string evaluation.
19
- const m=raw.match(/^(?:r|u)?(['"`])([^\r\n]*?)\1$/i);
20
- return m && !/[\\$\x00-\x1f]/.test(m[2]) && !/^[fFbB]/.test(raw) ? m[2] : null;
21
- };
22
- const fnTypes = new Set(['FunctionDefinition','FunctionDeclaration','FunctionExpression','ArrowFunction','MethodDeclaration']);
23
- const scopeTypes = new Set([...fnTypes,'Script','Block','ClassDefinition','ClassDeclaration']);
24
- function scope(node) { for(let p=node.parent;p;p=p.parent) if(scopeTypes.has(p.name)) return p; return node; }
25
- function scopeChain(node) { const out=[];for(let p=node;p;p=p.parent)if(scopeTypes.has(p.name))out.push(p);return out; }
26
- function namedFunction(node, body) {
27
- for(let p=node;p;p=p.parent) if(fnTypes.has(p.name)) {
28
- const n=children(p).find(c=>['VariableName','VariableDefinition','PropertyDefinition'].includes(c.name));
29
- if(n) return nodeText(n,body);
30
- if(p.name==='ArrowFunction' && p.parent?.name==='VariableDeclaration') return nodeText(children(p.parent).find(c=>c.name==='VariableDefinition'),body);
31
- }
32
- return null;
33
- }
34
- function riskyBranch(node) { for(let p=node.parent;p && p.name!=='Script';p=p.parent) if(/^(IfStatement|ForStatement|WhileStatement|TryStatement|SwitchStatement|MatchStatement)$/.test(p.name)) return true; return false; }
35
-
36
- export function parseCode(body, file) {
37
- const py=/\.py$/.test(file), parser=py?python:javascript.configure({dialect:'ts jsx'});
38
- const tree=parser.parse(body), nodes=[], issues=[], entries=[];
39
- const cursor=tree.cursor();
40
- do { nodes.push(cursor.node); if(nodes.length>50000) return {entries:[],issues:[{code:'syntax-budget',line:1}]}; } while(cursor.next());
41
- if(nodes.some(n=>n.type.isError)) return {entries:[],issues:[{code:'syntax-error',line:lineAt(body,nodes.find(n=>n.type.isError).from)}]};
42
- const txt=n=>nodeText(n,body), assignments=new Map();
43
- const key=(sc,name)=>`${sc.from}:${sc.to}:${name}`;
44
- const bind=(sc,name,value,at,ambiguous=false)=>{
45
- if(!identifier.test(name))return;
46
- const k=key(sc,name), a=assignments.get(k)||[];a.push({node:value,at,ambiguous});assignments.set(k,a);
47
- };
48
- for(const n of nodes) {
49
- const cs=children(n);
50
- if(['VariableDeclaration','AssignStatement','AssignmentExpression'].includes(n.name)) {
51
- for(let i=0;i<cs.length;i++) if(['Equals','AssignOp'].includes(cs[i].name)) {
52
- const left=cs.slice(0,i).filter(c=>['VariableDefinition','VariableName'].includes(c.name)).at(-1);
53
- if(left)bind(scope(n),txt(left),cs[i+1],n.from,riskyBranch(n));
54
- }
55
- }
56
- // Parameters shadow enclosing declarations, even when their value is unknown.
57
- if(n.name==='ParamList')for(const c of cs)if(['VariableDefinition','VariableName'].includes(c.name))bind(scope(n),txt(c),null,n.from,true);
58
- }
59
- function binding(name,node) {
60
- for(const sc of scopeChain(node)) {
61
- const a=assignments.get(key(sc,name));if(!a)continue;
62
- return a.length===1 && !a[0].ambiguous && a[0].at<node.from ? a[0].node : null;
63
- }
64
- return null;
65
- }
66
- const unresolved = node => [{unresolved:true,line:lineAt(body,node?.from||0)}];
67
- function evaluate(n, seen=new Set(), depth=0) {
68
- if(!n || depth>8 || seen.has(n.from))return unresolved(n);
69
- seen=new Set([...seen,n.from]);const raw=txt(n), cs=children(n), value=literal(raw);
70
- if(value!==null)return [{value,basis:'literal',line:lineAt(body,n.from)}];
71
- if(n.name==='ArrayExpression'||n.name==='TupleExpression')return cs.filter(c=>!['[',']','(',')',','].includes(c.name)).flatMap(c=>evaluate(c,seen,depth+1)).map(v=>({...v,candidate:true}));
72
- if(n.name==='VariableName') {
73
- if(sensitive(raw))return unresolved(n);
74
- const b=binding(raw,n);return b?evaluate(b,seen,depth+1).map(v=>({...v,basis:v.basis==='literal'?'reference':v.basis})):unresolved(n);
75
- }
76
- if(['ParenthesizedExpression','TypeAssertion','AsExpression'].includes(n.name))return evaluate(cs.find(c=>/Expression|VariableName|String/.test(c.name)),seen,depth+1);
77
- const env=raw.match(/^(?:process\.env|import\.meta\.env)\.([A-Z][A-Z0-9_]*)$/)||raw.match(/^(?:process\.env|os\.environ)\[['"]([A-Z][A-Z0-9_]*)['"]\]$/);
78
- if(env&&sensitive(env[1]))return unresolved(n);
79
- if(env)return [{env:env[1],basis:'environment-reference',line:lineAt(body,n.from)}];
80
- if(n.name==='MemberExpression') {
81
- const base=cs[0], member=cs.find(c=>c.name==='PropertyName'||c.name==='String');
82
- const b=base?.name==='VariableName'?binding(txt(base),n):null;
83
- const field=member?(literal(txt(member))??txt(member)):null;
84
- if(field&&sensitive(field))return unresolved(n);
85
- const props=b?pairs(b).filter(p=>p.key===field):[];
86
- if(props.length===1)return evaluate(props[0].node,seen,depth+1).map(v=>({...v,basis:'reference'}));
87
- }
88
- if(n.name==='CallExpression') {
89
- const call=txt(cs[0]),args=children(cs.find(c=>c.name==='ArgList')||n).filter(c=>!['(',')',','].includes(c.name));
90
- if(['os.getenv','os.environ.get'].includes(call)) {
91
- const field=literal(txt(args[0]));
92
- if(field && !sensitive(field) && /^[A-Z][A-Z0-9_]*$/.test(field))return [{env:field,basis:'environment-reference',line:lineAt(body,n.from)},...args.slice(1,2).flatMap(a=>evaluate(a,seen,depth+1)).map(v=>({...v,basis:'fallback',env:field}))];
93
- }
94
- }
95
- if(n.name==='BinaryExpression' && cs.some(c=>['??','||','or'].includes(txt(c))))return cs.filter(c=>!['LogicOp','or'].includes(c.name)).flatMap((c,i)=>evaluate(c,seen,depth+1).map(v=>({...v,...(i?{basis:'fallback'}:{})})));
96
- if(['ConditionalExpression'].includes(n.name))return cs.filter(c=>/Expression|String|VariableName/.test(c.name)).slice(1).flatMap(c=>evaluate(c,seen,depth+1).map(v=>({...v,basis:'branch'})));
97
- return unresolved(n);
98
- }
99
- function pairs(n) {
100
- const cs=children(n), out=[];
101
- if(n.name==='ObjectExpression')for(const c of cs.filter(c=>c.name==='Property')){
102
- const ps=children(c), colon=ps.findIndex(p=>p.name===':');
103
- if(colon>0)out.push({key:literal(txt(ps[0]))??txt(ps[0]),node:ps[colon+1],at:ps[0].from});
104
- else if(ps.length===1 && ps[0].name==='VariableName')out.push({key:txt(ps[0]),node:ps[0],at:ps[0].from});
105
- }
106
- if(['DictionaryExpression','ArgList'].includes(n.name))for(let i=1;i<cs.length-1;i++)if([':', 'AssignOp'].includes(cs[i].name))out.push({key:literal(txt(cs[i-1]))??txt(cs[i-1]),node:cs[i+1],at:cs[i-1].from});
107
- return out;
108
- }
109
- function privateContext(node) {
110
- for(let p=node;p;p=p.parent) {
111
- if(pairs(p).some(pair=>sensitive(pair.key)&&pair.node&&pair.node.from<=node.from&&pair.node.to>=node.to))return true;
112
- if(['VariableDeclaration','AssignStatement'].includes(p.name)) {
113
- const cs=children(p), at=cs.findIndex(c=>['Equals','AssignOp'].includes(c.name));
114
- if(at>0&&sensitive(txt(cs.slice(0,at).find(c=>['VariableName','VariableDefinition'].includes(c.name)))))return true;
115
- }
116
- }
117
- return false;
118
- }
119
- function emit(field,n,at,group,extra={}) {
120
- if(sensitive(field)||privateContext(n))return;
121
- const kind=endpointField(field)?'endpoint':modelField(field)?'model':taskField(field)?'task':null;if(!kind)return;
122
- const values=evaluate(n);
123
- for(const v of values) {
124
- if(v.unresolved){issues.push({code:'unresolved-expression',line:lineAt(body,at),field});continue;}
125
- entries.push({kind,field,...v,line:lineAt(body,at),group,task:namedFunction(n,body),candidate:candidateField(field)||v.candidate||false,...extra});
126
- }
127
- }
128
- for(const n of nodes) {
129
- if(privateContext(n))continue;
130
- const ps=pairs(n);for(const p of ps)emit(p.key,p.node,p.at,`object:${n.from}`);
131
- if(['VariableDeclaration','AssignStatement'].includes(n.name)){
132
- const cs=children(n),i=cs.findIndex(c=>['Equals','AssignOp'].includes(c.name)),left=cs.slice(0,i).find(c=>['VariableName','VariableDefinition'].includes(c.name));
133
- if(i>0&&left)emit(txt(left),cs[i+1],left.from,`assignment:${n.from}`);
134
- }
135
- if(n.name!=='CallExpression' && n.name!=='NewExpression')continue;
136
- const cs=children(n), callee=cs.find(c=>/Expression|VariableName/.test(c.name)),call=txt(callee),args=cs.find(c=>c.name==='ArgList');if(!args)continue;
137
- const named=[...pairs(args),...children(args).filter(c=>c.name==='ObjectExpression').flatMap(pairs)];
138
- const inferenceCall=/(?:chat\.completions\.create|responses\.create|messages\.create|embeddings\.create|generateContent|generateText|streamText|litellm\.(?:completion|acompletion|embedding)|(?:^|\.)(?:SentenceTransformer|pipeline|from_pretrained))$/.test(call);
139
- if(!inferenceCall)continue;
140
- let endpoints=named.filter(p=>endpointField(p.key)).flatMap(p=>evaluate(p.node).map(v=>({...v,line:lineAt(body,p.at),field:p.key})));
141
- const receiver=call.split('.')[0], b=binding(receiver,n);
142
- if(!endpoints.length && b && /^(CallExpression|NewExpression)$/.test(b.name)) {
143
- const a=children(b).find(c=>c.name==='ArgList');
144
- if(a)endpoints=[...pairs(a),...children(a).filter(c=>c.name==='ObjectExpression').flatMap(pairs)].filter(p=>endpointField(p.key)).flatMap(p=>evaluate(p.node).map(v=>({...v,line:lineAt(body,p.at),field:p.key})));
145
- }
146
- const group=`call:${n.from}`,task=namedFunction(n,body);
147
- if(task)entries.push({kind:'task',field:'function',value:task,basis:'call-site',line:lineAt(body,n.from),group,task,endpoints});
148
- for(const p of named.filter(p=>modelField(p.key)||endpointField(p.key)||taskField(p.key)))emit(p.key,p.node,p.at,group,{task,endpoints,inferenceCall:true});
149
- if(/(?:SentenceTransformer|from_pretrained)$/.test(call)){
150
- const arg=children(args).find(c=>!['(',')',','].includes(c.name));if(arg)emit('model',arg,arg.from,group,{task,endpoints:[],localLibrary:true,inferenceCall:true});
151
- }
152
- }
153
- return {entries,issues};
154
- }
155
-
156
- export function parseConfig(body,file) {
157
- const entries=[],issues=[];
158
- const add=(field,value,line,group,extra={})=>{
159
- if(sensitive(field))return;
160
- const kind=endpointField(field)?'endpoint':modelField(field)?'model':taskField(field)?'task':null;if(!kind)return;
161
- if(typeof value!=='string')return;
162
- const env=value.match(/^\$\{([A-Z][A-Z0-9_]*)\}$/)||value.match(/^os\.environ\/([A-Z][A-Z0-9_]*)$/);
163
- if(env)entries.push({kind,field,env:env[1],basis:'environment-reference',line,group,...extra});
164
- else if(/[$`\r\n\x00-\x1f]/.test(value))issues.push({code:'unresolved-expression',line,field});
165
- else entries.push({kind,field,value,basis:'literal',line,group,candidate:candidateField(field),...extra});
166
- };
167
- if(/(?:^|\/)\.env(?:\.[\w.-]+)?$/.test(file)) {
168
- for(const [i,line]of body.split('\n').entries()){
169
- const m=line.match(/^\s*(?:export\s+)?([A-Za-z_][\w]*)\s*=\s*(.*?)\s*$/);if(!m)continue;
170
- const raw=m[2].replace(/\s+#.*$/,'');const value=/^["']/.test(raw)?literal(raw):raw;
171
- if(value!==null) {
172
- if(!sensitive(m[1])&&!endpointField(m[1])&&!modelField(m[1])&&!taskField(m[1])&&value.length<=2048&&!/[$`\r\n\x00-\x1f]/.test(value))entries.push({kind:'binding',field:m[1],value,line:i+1,environmentDeclaration:true});
173
- else add(m[1],value,i+1,`env:${i}`,{environmentDeclaration:true});
174
- }
175
- }
176
- return {entries,issues};
177
- }
178
- const walk=(value,path=[],line=1,locate=()=>line)=>{
179
- if(!value||typeof value!=='object')return;
180
- if(path.length>40){issues.push({code:'syntax-budget',line});return;}
181
- for(const [field,v]of Object.entries(value)){
182
- if(sensitive(field))continue;
183
- const group=path.join('.'),at=locate(field,typeof v==='string'?v:null);
184
- if(Array.isArray(v))for(const item of v){if(typeof item==='string')add(field,item,at,group,{candidate:true});else walk(item,[...path,field,String(v.indexOf(item))],at,locate);}
185
- else if(typeof v==='string')add(field,v,at,group);
186
- else walk(v,[...path,field],at,locate);
187
- }
188
- };
189
- try {
190
- if(/\.toml$/.test(file)) {
191
- TOML.parse(body); // Validate the complete document before extracting fields.
192
- let table='root', serial=0, multiline=null;
193
- for(const [i,raw] of body.split('\n').entries()) {
194
- if(multiline){if(raw.includes(multiline))multiline=null;continue;}
195
- const triple=raw.match(/(['"]){3}/)?.[0];
196
- if(triple&&raw.indexOf(triple)===raw.lastIndexOf(triple)){multiline=triple;continue;}
197
- const section=raw.match(/^\s*(\[\[?.*?\]\]?)\s*(?:#.*)?$/);
198
- if(section){table=`${section[1]}:${++serial}`;continue;}
199
- const field=raw.match(/^\s*([A-Za-z0-9_-]+|"[^"\\]*"|'[^']*')\s*=\s*(.+)$/);
200
- if(!field)continue;
201
- let value;
202
- try {value=TOML.parse(`value = ${field[2]}`).value;}catch{
203
- if(endpointField(field[1])||modelField(field[1])||taskField(field[1]))issues.push({code:'unresolved-expression',line:i+1,field:field[1]});
204
- continue;
205
- }
206
- const key=field[1].replace(/^['"]|['"]$/g,'');
207
- if(sensitive(key))continue;
208
- if(typeof value==='string')add(key,value,i+1,table);
209
- else if(Array.isArray(value))for(const [j,item] of value.entries()) {
210
- if(typeof item==='string')add(key,item,i+1,table,{candidate:true});
211
- else walk(item,[table,key,String(j)],i+1);
212
- }
213
- else walk(value,[table,key],i+1);
214
- }
215
- } else {
216
- // JSON is a YAML subset, but strict JSON syntax is checked independently.
217
- if(/\.json$/.test(file))JSON.parse(body);
218
- const lines=new LineCounter(),doc=parseDocument(body,{lineCounter:lines,logLevel:'silent',uniqueKeys:true,customTags:[],schema:'core'});
219
- if(doc.errors.length)throw Error('Invalid configuration');
220
- const visit=(node,path=[],depth=0)=>{
221
- if(depth>40){issues.push({code:'syntax-budget',line:1});return;}
222
- if(isAlias(node)){issues.push({code:'unresolved-alias',line:lines.linePos(node.range?.[0]||0).line});return;}
223
- if(isSeq(node)){for(const [i,item] of node.items.entries())visit(item,[...path,String(i)],depth+1);return;}
224
- if(isMap(node))for(const pair of node.items){
225
- if(!isScalar(pair.key)||typeof pair.key.value!=='string')continue;
226
- const field=pair.key.value;if(sensitive(field))continue;
227
- const at=lines.linePos(pair.key.range?.[0]||0).line,group=path.join('.');
228
- if(isScalar(pair.value))add(field,pair.value.value,at,group,{alias:field==='model_name'&&node.items.some(p=>isScalar(p.key)&&p.key.value==='litellm_params')});
229
- else if(isSeq(pair.value))for(const [i,item]of pair.value.items.entries()){if(isScalar(item))add(field,item.value,lines.linePos(item.range?.[0]||0).line,group,{candidate:true});else visit(item,[...path,field,String(i)],depth+1);}
230
- else visit(pair.value,[...path,field],depth+1);
231
- }
232
- };visit(doc.contents);
233
- }
234
- }catch{issues.push({code:'syntax-error',line:1});}
235
- return {entries,issues};
236
- }
@@ -1,15 +0,0 @@
1
- import { parentPort, workerData } from 'node:worker_threads';
2
- import { createRepositoryScanner } from './repository-index.mjs';
3
- const scan=createRepositoryScanner(workerData.root,workerData.limits);
4
- let busy=false;
5
- parentPort.on('message',async message=>{
6
- if(message!=='scan'||busy)return;
7
- busy=true;
8
- try {
9
- const index=await scan({onProgress:update=>parentPort.postMessage({type:'progress',...update})});
10
- parentPort.postMessage({type:'complete',index});
11
- }catch {
12
- // Do not send exception messages: filesystem errors can contain paths.
13
- parentPort.postMessage({type:'failed'});
14
- }finally {busy=false;}
15
- });
@@ -1,171 +0,0 @@
1
- // Deterministic, deliberately fictional data. Never mixed with local sources.
2
- // Rates, latencies, allocations, and utilization are illustrative, not benchmarks.
3
- const GiB = 1024 ** 3;
4
- const TEAMS = [
5
- ['Support', ['billing escalation', 'refund queue', 'onboarding tickets', 'SLA exceptions', 'customer handoff', 'incident follow-up', 'CSAT themes']],
6
- ['Engineering', ['checkout regression', 'API migration', 'dependency upgrade', 'flaky test suite', 'release notes', 'auth refactor', 'performance budget']],
7
- ['Research', ['competitor landscape', 'market signals', 'customer interviews', 'pricing study', 'industry brief', 'source verification', 'product opportunities']],
8
- ['Finance', ['invoice exceptions', 'expense reconciliation', 'renewal forecast', 'vendor spending', 'revenue variance', 'purchase approvals', 'quarterly narrative']],
9
- ['Security', ['access review', 'dependency advisory', 'policy exceptions', 'incident timeline', 'threat brief', 'audit evidence', 'service permissions']],
10
- ['Operations', ['capacity forecast', 'deployment checklist', 'on-call handoff', 'runbook updates', 'queue anomalies', 'availability report', 'change requests']],
11
- ['Sales', ['account research', 'proposal draft', 'pipeline review', 'meeting preparation', 'lead enrichment', 'deal risks', 'renewal outreach']],
12
- ['Legal', ['supplier terms', 'contract changes', 'policy updates', 'obligation register', 'clause comparison', 'document redaction', 'agreement summary']],
13
- ['Data', ['schema changes', 'quality checks', 'warehouse queries', 'catalog enrichment', 'metric definitions', 'report narrative', 'lineage review']],
14
- ['Knowledge', ['document ingestion', 'retrieval quality', 'index refresh', 'duplicate articles', 'citation coverage', 'search relevance', 'content freshness']],
15
- ];
16
- // name, provider/runtime, location, typical latency, fictional rate multiplier, role
17
- const MODEL_DEFINITIONS = [
18
- ['GPT-5', 'OpenAI', 'cloud', 3600, 2.2, 'generation'],
19
- ['Claude Sonnet 4.5', 'Anthropic', 'cloud', 4200, 2.6, 'generation'],
20
- ['Gemini 2.5 Flash', 'Google', 'cloud', 900, .3, 'generation'],
21
- ['DeepSeek R1', 'Baseten', 'cloud', 14000, .8, 'reasoning'],
22
- ['Mistral Medium 3', 'Mistral', 'cloud', 1800, .7, 'generation'],
23
- ['GPT-5 mini', 'OpenAI', 'cloud', 1400, .4, 'generation'],
24
- ['Claude Opus 4.1', 'Anthropic', 'cloud', 11000, 6, 'reasoning'],
25
- ['Gemini 2.5 Pro', 'Google', 'cloud', 7000, 1.8, 'reasoning'],
26
- ['GPT-5 nano', 'OpenAI', 'cloud', 480, .08, 'generation'],
27
- ['Claude Haiku 3.5', 'Anthropic', 'cloud', 700, .2, 'generation'],
28
- ['gpt-oss-120b', 'Groq', 'cloud', 2000, .25, 'generation'],
29
- ['qwen-3.8-27b', 'Cerebras', 'cloud', 2400, .65, 'generation'],
30
- ['text-embedding-3-large', 'OpenAI', 'cloud', 160, .02, 'embedding'],
31
- ['embed-v4', 'Cohere', 'cloud', 190, .025, 'embedding'],
32
- ['Qwen3 · 8B', 'Ollama', 'local', 1400, 0, 'generation'],
33
- ['Qwen3 · 30B', 'Ollama', 'local', 4200, 0, 'reasoning'],
34
- ['Llama 3.3 · 70B', 'vLLM', 'local', 3100, 0, 'generation'],
35
- ['Gemma 3 · 27B', 'LM Studio', 'local', 2600, 0, 'generation'],
36
- ['DeepSeek R1 · 14B', 'Ollama', 'local', 6200, 0, 'reasoning'],
37
- ['nomic-embed-text', 'Ollama', 'local', 90, 0, 'embedding'],
38
- ['bge-reranker-v2-m3', 'vLLM', 'local', 75, 0, 'reranking'],
39
- ];
40
- // Explicit fictional metadata, never inferred from the serving company.
41
- const MODEL_CREATORS = ['OpenAI', 'Anthropic', 'Google', 'DeepSeek', 'Mistral', 'OpenAI', 'Anthropic', 'Google', 'OpenAI', 'Anthropic', 'OpenAI', 'Qwen', 'OpenAI', 'Cohere', 'Qwen', 'Qwen', 'Meta', 'Google', 'DeepSeek', 'Nomic', 'BAAI'];
42
- const ROUTERS = ['router.quality', 'router.latency', 'router.cost', 'router.private'];
43
- const GATEWAYS = ['llm-gateway', 'llm-gateway-eu', 'batch-proxy'];
44
- const ROLES = ['Intake', 'Research', 'Delivery', 'Review'];
45
- const MODEL_PATHS = [
46
- ['direct', 'router'], ['gateway', 'router'], ['direct', 'gateway'],
47
- ['router', 'gateway'], ['direct'], ['direct', 'router', 'gateway'],
48
- ['gateway'], ['router'], ['direct'], ['gateway', 'router'],
49
- ['gateway', 'direct'], ['gateway'], ['direct'], ['gateway'],
50
- ['local', 'router'], ['local', 'router'], ['local', 'gateway'],
51
- ['local'], ['router', 'local'], ['local'], ['local', 'gateway'],
52
- ];
53
- const TEAM_MODELS = [
54
- [2, 5, 8, 9, 14], [0, 1, 3, 5, 15, 16], [3, 6, 7, 10, 11],
55
- [0, 4, 7, 14, 17], [1, 3, 6, 15, 18], [2, 4, 5, 14, 16, 18],
56
- [1, 2, 5, 9, 11], [0, 1, 6, 7, 17], [0, 5, 10, 14, 15, 16],
57
- [2, 8, 11, 14, 17],
58
- ];
59
-
60
- export function sampleSnapshot(now = Date.now()) {
61
- let seed = 41;
62
- const random = () => { seed = (seed * 16807) % 2147483647; return (seed - 1) / 2147483646; };
63
- const choose = values => values[Math.floor(random() * values.length)];
64
- const agents = TEAMS.flatMap(([team], teamIndex) => ROLES.map((role, index) => ({ id: `${team.toLowerCase()}.${role.toLowerCase()}`, name: index === 3 ? null : `${team.toLowerCase()}.${role.toLowerCase()}` })));
65
- const tasks = TEAMS.flatMap(([, subjects], teamIndex) => subjects.flatMap((subject, index) => ['Review', 'Prepare'].map((verb, variant) => ({ id: `task-${teamIndex * 14 + index * 2 + variant + 1}`, name: index % 3 === 0 ? null : `${subject.replaceAll(' ', '_').toLowerCase()}.${verb.toLowerCase()}`, teamIndex, agent: agents[teamIndex * 4 + (index * 2 + variant) % 4] }))));
66
- let totalTaskWeight = 0;
67
- const taskWeights = tasks.map((task, index) => { totalTaskWeight += [1.7, 1.5, .8, .65, 1.1, 1.4, .9, .6, 1.2, 1.8][task.teamIndex] * (.5 + index % 7 * .25); return totalTaskWeight; });
68
- const pickTask = () => { const target = random() * totalTaskWeight; return tasks[taskWeights.findIndex(weight => weight > target)]; };
69
- const records = [];
70
- const weights = Array.from({ length: 96 }, (_, bin) => 30 + 80 * (1 + Math.sin((bin - 24) / 15)) + (bin >= 67 && bin <= 74 ? 170 : 0) + random() * 45);
71
- const totalWeight = weights.reduce((sum, value) => sum + value, 0);
72
- let cumulative = 0;
73
- for (let bin = 0; bin < 96; bin++) {
74
- cumulative += weights[bin];
75
- const end = Math.round(cumulative / totalWeight * 9600);
76
- while (records.length < end) {
77
- const index = records.length;
78
- const running = index >= 9576;
79
- const task = running ? choose(tasks.filter(task => task.agent.id === agents[(index - 9576) * 7 % agents.length].id)) : index < tasks.length ? tasks[index * 53 % tasks.length] : pickTask();
80
- // Cover every catalog entry, then vary model mix with retrieval and generation.
81
- const modelIndex = index < 21 ? index : [2, 8, 9].includes(task.teamIndex) && index % 9 === 0 ? 20 : index % 11 === 0 ? [12, 13, 19][task.teamIndex % 3] : choose(TEAM_MODELS[task.teamIndex]);
82
- const [model, provider, location, baseLatency, rate, role] = MODEL_DEFINITIONS[modelIndex];
83
- const pick = random();
84
- const routeKind = pick < .09 && [4, 11, 13, 20].includes(modelIndex) ? 'unknown' : choose(MODEL_PATHS[modelIndex]);
85
- const route = routeKind === 'unknown' ? 'Unattributed route' : routeKind === 'local' ? provider : routeKind === 'direct' ? `${provider} API` : routeKind === 'router' ? location === 'local' ? 'router.private' : choose(ROUTERS.slice(0, 3)) : choose(GATEWAYS);
86
- const failed = !running && random() < (bin >= 68 && bin <= 73 ? .085 : .012);
87
- const inputTokens = Math.round(320 + random() ** 2 * (role === 'embedding' ? 32000 : 24000));
88
- const outputTokens = running ? null : failed || role === 'embedding' || role === 'reranking' ? 0 : Math.round(60 + random() ** 2 * (role === 'reasoning' ? 6500 : 2400));
89
- const cacheReadTokens = role === 'embedding' || role === 'reranking' ? 0 : Math.round(inputTokens * (index % 5 === 0 ? .7 : random() * .35));
90
- const cost = location === 'local' || running || index % 19 === 0 ? null : ((inputTokens - cacheReadTokens * .8) + outputTokens * 4) * rate / 1_000_000;
91
- const occurredAt = running ? now - (9600 - index) * 2300 : now - (96 - bin) * 900000 + random() * 900000;
92
- records.push({ id: `sample-request-${index + 1}`, sourceId: 'sample-activity', requestId: `req-${String(index + 1).padStart(5, '0')}`, occurredAt: new Date(occurredAt).toISOString(), model, modelCreator: MODEL_CREATORS[modelIndex], provider, location, workload: `services/${TEAMS[task.teamIndex][0].toLowerCase()}`, scope: 'agent', agentId: task.agent.id, agent: task.agent.name, taskId: task.id, task: task.name, route, routeKind, status: running ? 'running' : failed ? 'failed' : 'completed', inputTokens, outputTokens, cacheReadTokens, durationMs: running || index % 43 === 0 ? null : Math.round(baseLatency * (.4 + random() * 1.4) * (index % 71 === 0 ? 5 : 1)), cost, currency: 'USD', costKind: index % 9 === 0 ? 'estimated' : 'reported' });
93
- }
94
- }
95
- const time = new Date(now).toISOString();
96
- const localMemory = [5.2, 18.6, 42.4, 16.8, 9.1, .27, 1.2];
97
- const runningModels = new Set(records.filter(item => item.status === 'running').map(item => item.model));
98
- const models = MODEL_DEFINITIONS.map(([name, provider, location], index) => {
99
- const loaded = location === 'local' && (index !== 15 && index !== 18 || runningModels.has(name));
100
- return { id: `sample-model-${index}`, name, modelCreator: MODEL_CREATORS[index], creatorSources: ['sample-activity'], provider, location, state: location === 'cloud' ? 'observed' : loaded ? 'loaded' : 'installed', sourceId: location === 'cloud' ? 'sample-activity' : provider === 'Ollama' ? 'ollama' : provider === 'vLLM' ? 'runtime-vllm' : 'runtime-studio', memoryBytes: loaded ? localMemory[index - 14] * GiB : null, parameters: index >= 14 ? ['8B', '30B', '70B', '27B', '14B', '137M', '568M'][index - 14] : null, quantization: index >= 14 ? index >= 19 ? 'F16' : 'Q4_K_M' : null };
101
- });
102
- const modelCounts = new Map();
103
- for (const record of records) modelCounts.set(`${record.provider}:${record.model}`, (modelCounts.get(`${record.provider}:${record.model}`) ?? 0) + 1);
104
- models.sort((a, b) => modelCounts.get(`${b.provider}:${b.name}`) - modelCounts.get(`${a.provider}:${a.name}`));
105
- const history = Array.from({ length: 240 }, (_, index) => ({ at: new Date(now - (239 - index) * 15000).toISOString(), cpuPercent: Math.min(97, 46 + Math.sin(index / 13) * 15 + (index > 155 && index < 184 ? 24 : 0) + random() * 12), memoryUsed: (110 + Math.sin(index / 32) * 12 + (index > 155 ? 18 : 0) + random() * 2) * GiB }));
106
- const providers = [...new Set(models.filter(model => model.location === 'cloud').map(model => model.provider))];
107
- const poolId = name => `pool-${name.toLowerCase().replaceAll(' ', '-')}`;
108
- const deployments = MODEL_DEFINITIONS.flatMap(([model, provider, location], index) => Array.from({ length: index < 6 ? 2 : 1 }, (_, variant) => ({ id: `deployment-${index}-${variant}`, name: location === 'local' ? `${provider.toLowerCase().replaceAll(' ', '-')}-local` : `${provider.toLowerCase()}-${variant ? 'eu-1' : 'us-1'}`, model, modelCreator: MODEL_CREATORS[index], servingMode: location === 'local' ? 'self-hosted' : provider === 'Baseten' ? 'dedicated' : 'shared', provider, location, state: location === 'local' ? models.find(item => item.name === model).state : 'configured', sourceId: location === 'cloud' ? 'inventory' : models.find(item => item.name === model).sourceId, poolIds: [poolId(provider)] })));
109
- records.forEach((record, index) => {
110
- const modelIndex = MODEL_DEFINITIONS.findIndex(([model]) => model === record.model);
111
- record.deploymentId = record.routeKind === 'unknown' ? null : `deployment-${modelIndex}-${modelIndex < 6 && index % 4 === 0 ? 1 : 0}`;
112
- record.path = record.routeKind === 'unknown' ? [] : [{ name: record.route, kind: record.routeKind }];
113
- if (record.routeKind === 'router') record.path.push(record.location === 'local' ? { name: record.provider, kind: 'local' } : { name: index % 4 === 0 ? 'llm-gateway-eu' : 'llm-gateway', kind: 'gateway' });
114
- if (['router', 'gateway'].includes(record.routeKind) && record.location === 'cloud') record.path.push({ name: `${record.provider} API`, kind: 'direct' });
115
- if (record.routeKind === 'gateway' && record.location === 'local') record.path.push({ name: record.provider, kind: 'local' });
116
- });
117
- const capacityPools = [
118
- ...providers.map((name, index) => ({ id: poolId(name), name: `${name.toLowerCase()}-project-01`, kind: 'quota', sourceId: 'sample-capacity', windowSeconds: 60, observedAt: name === 'Cohere' ? null : new Date(now - (name === 'Baseten' ? 180000 : 12000)).toISOString(), resetAt: new Date(now + 28000).toISOString(), metrics: name === 'Cohere' ? {} : { requestsUsed: [842, 318, 126, 427, 193, 256][index % 6], requestsLimit: 1000, tokensUsed: name === 'Anthropic' ? 882000 : 180000 + index * 71000, tokensLimit: 1000000, running: 8 + index * 3, concurrencyLimit: 80 } })),
119
- ...['Ollama', 'vLLM', 'LM Studio'].map((name, index) => ({ id: poolId(name), name: `${name} / host unspecified`, kind: 'runtime', sourceId: 'sample-capacity', observedAt: new Date(now - 8000).toISOString(), metrics: { running: [8, 18, 3][index], waiting: [2, 7, 0][index], ...(index < 2 ? { concurrencyLimit: [12, 24][index], cachePercent: [54, 86][index] } : {}), memoryUsedBytes: [33.2, 43.6, 16.8][index] * GiB, memoryTotalBytes: [64, 64, 24][index] * GiB } })),
120
- ...GATEWAYS.map((name, index) => ({ id: poolId(name), name, kind: 'gateway', sourceId: 'sample-capacity', observedAt: new Date(now - 11000).toISOString(), metrics: { running: [46, 14, 6][index], concurrencyLimit: [50, 40, 20][index], waiting: [4, 0, 0][index] } })),
121
- ];
122
- const hostDefinitions = [
123
- ['this-machine', 'devbox-01', '', '', 'AMD EPYC · 32 cores', 32, 256, ['RTX 4090', 24, 1], ['LM Studio']],
124
- ['gpu-01', 'gpu-01', 'dc-01', 'rack-07', 'AMD EPYC · 64 cores', 64, 256, ['NVIDIA L40S', 48, 2], ['Ollama']],
125
- ['gpu-02', 'gpu-02', 'dc-01', 'rack-07', 'AMD EPYC · 96 cores', 96, 512, ['NVIDIA H100', 80, 2], ['vLLM']],
126
- ['gpu-03', 'gpu-03', 'dc-01', 'rack-07', 'AMD EPYC · 64 cores', 64, 256, ['NVIDIA L40S', 48, 2], []],
127
- ['gpu-04', 'gpu-04', 'dc-02', 'rack-03', 'Intel Xeon · 48 cores', 48, 256, ['NVIDIA A100', 80, 2], []],
128
- ['gpu-05', 'gpu-05', 'dc-02', 'rack-03', 'Intel Xeon · 48 cores', 48, 256, ['NVIDIA A100', 80, 2], []],
129
- ];
130
- const hosts = hostDefinitions.map(([id, name, site, rack, chip, cores, ram, [gpu, vram, count], runtimes], index) => {
131
- const observedAt = index === 5 ? null : new Date(now - (index === 4 ? 180000 : 0)).toISOString();
132
- const hostHistory = observedAt ? history.map((point, i) => ({ at: new Date(Date.parse(point.at) - (index === 4 ? 180000 : 0)).toISOString(), cpuPercent: Math.max(0, Math.min(100, point.cpuPercent * [.55, 1, 1.2, .12, .4][index] + Math.sin(i / (11 + index)) * 4)), memoryUsed: point.memoryUsed * [.65, 1, 1.6, .15, .8][index] })) : [];
133
- const loadedModels = models.filter(model => model.state === 'loaded' && runtimes.includes(model.provider)).map(model => ({ ...model, runtime: model.provider, memoryKind: 'accelerator', deploymentId: deployments.find(deployment => deployment.model === model.name)?.id }));
134
- return { id, name, hostname: name, nameField: 'hostname', site, rack, chip, cores, platform: 'linux', arch: 'x64', sourceId: id === 'this-machine' ? 'machine' : 'hardware', observedAt, memoryTotal: ram * GiB, memoryUsed: hostHistory.at(-1)?.memoryUsed ?? null, cpuPercent: hostHistory.at(-1)?.cpuPercent ?? null, history: hostHistory, loadedModels, residencyReported: Boolean(observedAt),
135
- accelerators: Array.from({ length: count }, (_, device) => ({ id: `gpu-${device}`, name: gpu, memoryTotal: vram * GiB, memoryUsed: observedAt ? ([17.8, 35.2, 52.6, 1.2, 12][index] / count) * GiB : null, utilization: observedAt ? [32, 68, 84, 0, 21][index] + device : null })) };
136
- });
137
- for (const deployment of deployments.filter(item => item.location === 'local')) {
138
- const host = hosts.find(item => item.loadedModels.some(model => model.name === deployment.model));
139
- if (host) { deployment.hostId = host.id; deployment.name = `${host.name} / ${deployment.provider}`; deployment.location = host.id === 'this-machine' ? 'local' : 'private'; }
140
- }
141
- const privateModels = new Set(deployments.filter(item => item.location === 'private').map(item => `${item.provider}:${item.model}`));
142
- for (const model of models) if (privateModels.has(`${model.provider}:${model.name}`)) model.location = 'private';
143
- for (const record of records) if (privateModels.has(`${record.provider}:${record.model}`)) record.location = 'private';
144
- for (const pool of capacityPools.filter(item => item.kind === 'runtime')) {
145
- const host = hosts.find(item => item.loadedModels.some(model => pool.id === poolId(model.provider)));
146
- if (host) pool.name = `${pool.name.split(' / ')[0]} / ${host.name}`;
147
- }
148
- return { schemaVersion: 1, mode: 'sample', observedAt: time, interval: 15, project: { name: 'inference-sample', scope: 'Enterprise sample environment' }, models, deployments, capacityPools, hosts,
149
- runtimeStats: [{ name: 'vLLM', sourceId: 'runtime-vllm', hostId: 'gpu-02', running: 18, waiting: 7, inputTokens: 48200000, outputTokens: 9400000, finished: 74620 }],
150
- inventory: [
151
- ...['Baseten', 'Groq', 'Cerebras'].map(name => ({ name, kind: 'provider', location: 'cloud', state: 'configured', sourceId: 'environment', evidence: [{ field: `${name.toUpperCase()}_API_KEY`, basis: 'credential-name' }] })),
152
- ...ROUTERS.map(name => ({ name, kind: 'router', location: name === 'router.private' ? 'local' : 'cloud', state: 'observed' })),
153
- ...GATEWAYS.map(name => ({ name, kind: 'gateway', location: 'cloud', state: 'observed', poolIds: [poolId(name)] })),
154
- ...['Ollama', 'vLLM', 'LM Studio'].map(name => ({ name, kind: 'runtime', location: name === 'LM Studio' ? 'local' : 'private', state: 'observed' })),
155
- ], records,
156
- sources: [
157
- { id: 'environment', name: 'Service configuration', kind: 'Process environment', state: 'connected', updatedAt: time, detail: 'BASETEN_API_KEY · GROQ_API_KEY · CEREBRAS_API_KEY', note: 'Fictional examples of environment-variable evidence. No credentials were read for this sample. Configuration alone does not establish usage, deployments, or hardware.' },
158
- { id: 'hardware', name: 'Hardware export', kind: 'Local file', state: 'partial', updatedAt: time, detail: '.openmerit/hosts.json', note: 'Fictional fleet across two racks and a workstation. gpu-04 has stale readings; gpu-05 has not reported a reading.' },
159
- { id: 'sample-capacity', name: 'Capacity export', kind: 'Local file', state: 'partial', updatedAt: time, detail: '.openmerit/capacity.json', note: 'Fictional shared quotas and runtime readings. Baseten is stale; Cohere has no reading. Pool measurements cover all consumers, including work outside this project.' },
160
- { id: 'sample-activity', name: 'Inference activity', kind: 'Local file', state: 'connected', updatedAt: time, detail: '.openmerit/inference.jsonl', note: 'Fictional metadata for 40 agent IDs and 140 task IDs. Names represent exported fields, not discovered code. Some display names are absent. Measurements and prices are illustrative.', records: records.length },
161
- { id: 'ollama', name: 'Ollama', kind: 'Runtime export', state: 'connected', updatedAt: time, detail: 'gpu-01 / Ollama', note: 'Fictional private-runtime inventory and resident allocations from existing exports.' },
162
- { id: 'runtime-studio', name: 'LM Studio', kind: 'Local runtime', state: 'connected', updatedAt: time, detail: '127.0.0.1:1234', note: 'Fictional compatible runtime inventory.' },
163
- { id: 'runtime-vllm', name: 'vLLM', kind: 'Runtime export', state: 'connected', updatedAt: time, detail: 'gpu-02 / vLLM', note: 'Fictional runtime and host exports. Remote endpoints are not queried by this terminal.' },
164
- { id: 'machine', name: 'This machine', kind: 'Operating system', state: 'connected', updatedAt: time, detail: 'Linux · x64', note: 'Fictional whole-machine CPU and memory samples. These are not attributed to individual requests.' },
165
- { id: 'openmerit', name: 'OpenMerit', kind: 'Project evidence', state: 'connected', updatedAt: time, detail: '.openmerit/events.jsonl', note: 'Fictional audit source. The activity export takes precedence; request totals are not added together.', records: 140 },
166
- { id: 'inventory', name: 'Declared inventory', kind: 'Project configuration', state: 'connected', updatedAt: time, detail: '.openmerit/terminal.json', note: 'Fictional configuration keys for four routers, three gateways, and three local runtimes. llm-gateway is an example service name, not a built-in gateway.' },
167
- { id: 'batch-archive', name: 'Batch archive', kind: 'Local file', state: 'unavailable', updatedAt: null, detail: '.openmerit/archive/inference.jsonl', note: 'Fictional unavailable source. No requests from this archive are included.' },
168
- ],
169
- machine: hosts[0],
170
- };
171
- }
@@ -1,12 +0,0 @@
1
- export type ScoutClient = 'claude-code' | 'pi';
2
- export type ScoutScope = 'user' | 'project';
3
- export interface DeviceCredentials {
4
- version: 1;
5
- server: string;
6
- key: string;
7
- keyId?: string;
8
- headers?: Record<string, string>;
9
- }
10
- export declare function credentialsPath(client: ScoutClient, scope: ScoutScope, root: string): string;
11
- export declare function pendingPath(client: ScoutClient, scope: ScoutScope, root: string): string;
12
- export declare function readCredentials(path: string): DeviceCredentials | null;
@@ -1,44 +0,0 @@
1
- import { createHash } from 'node:crypto';
2
- import { readFileSync } from 'node:fs';
3
- import { homedir } from 'node:os';
4
- import { dirname, join, resolve } from 'node:path';
5
-
6
- export type ScoutClient = 'claude-code' | 'pi';
7
- export type ScoutScope = 'user' | 'project';
8
- export interface DeviceCredentials {
9
- version: 1;
10
- server: string;
11
- key: string;
12
- keyId?: string;
13
- headers?: Record<string, string>;
14
- }
15
-
16
- // Retain the existing Claude user key in place. Other installations own their
17
- // own key and approval, so removing one cannot revoke another's connection.
18
- export function credentialsPath(client: ScoutClient, scope: ScoutScope, root: string): string {
19
- const base = resolve(process.env.OPENMERIT_CONFIG_DIR ?? join(process.env.XDG_CONFIG_HOME ?? join(homedir(), '.config'), 'openmerit'));
20
- if (client === 'claude-code' && scope === 'user') return join(base, 'credentials.json');
21
- const installation = scope === 'project' ? createHash('sha256').update(resolve(root)).digest('hex').slice(0, 24) : 'user';
22
- return join(base, client, installation, 'credentials.json');
23
- }
24
-
25
- export function pendingPath(client: ScoutClient, scope: ScoutScope, root: string): string {
26
- return join(dirname(credentialsPath(client, scope, root)), 'login.json');
27
- }
28
-
29
- export function readCredentials(path: string): DeviceCredentials | null {
30
- let value: unknown;
31
- try { value = JSON.parse(readFileSync(path, 'utf8')); }
32
- catch (error) { if (error instanceof Error && 'code' in error && error.code === 'ENOENT') return null; throw new Error('Scout credentials could not be read. Run the targeted install command again.'); }
33
- if (!value || typeof value !== 'object' || !('version' in value) || value.version !== 1
34
- || !('server' in value) || typeof value.server !== 'string'
35
- || !('key' in value) || typeof value.key !== 'string' || !/^om_[A-Za-z0-9_-]{43}$/.test(value.key)) return null;
36
- const url = new URL(value.server);
37
- if (url.username || url.password || (url.protocol !== 'https:' && !(url.protocol === 'http:' && ['127.0.0.1', 'localhost', '[::1]'].includes(url.hostname)))) return null;
38
- const result: DeviceCredentials = { version: 1, server: value.server.replace(/\/+$/, ''), key: value.key };
39
- if ('keyId' in value && typeof value.keyId === 'string') result.keyId = value.keyId;
40
- if ('headers' in value && value.headers && typeof value.headers === 'object') {
41
- result.headers = Object.fromEntries(Object.entries(value.headers).filter(([key, v]) => /^X-Scout-[A-Za-z-]+$/.test(key) && typeof v === 'string'));
42
- }
43
- return result;
44
- }
@@ -1,5 +0,0 @@
1
- import type { ScoutClient } from './scout-credentials.ts';
2
- export declare const rule = "# Application model decisions \u00B7 OpenMerit Scout\n\nBefore choosing, recommending, adding, downloading, replacing or configuring\nANY application inference model, load the `openmerit-scout` skill and consult\n`scout_models` on the `openmerit` MCP server before committing to a model.\nThis applies even when the user only asks you to build a feature, never mentions\nmodels or Scout, or names a model themselves (assess it; do not silently replace it).\n\nA pretrained model, local weights, browser inference, an SDK's default model,\nembeddings, OCR, speech, image classification, detection and segmentation all\ncount as model choices, not implementation details. Identify this decision before\nwriting model-specific code, installing a model package or downloading weights.\n\nFollow the skill's project access policy. If it is missing, ask and save it first.\nScout may answer with clarifying questions: relay them to the user and continue\nthe same Scout conversation with their answers before choosing.\nIf Scout is unavailable, fails or has no suitable evidence/candidate, disclose the\ngap and pause the model decision; do not fall back to a remembered or SDK-default\nmodel. You may continue work that does not commit to a model.\n\nDo not consult Scout for unrelated edits, normal inference calls using an unchanged\napproved model, or this coding agent's own model. Do not reselect approved models\nunless the task introduces or changes a model decision.\n\nWhen the user says \"update openmerit\", run\n`npx -y openmerit@latest update --scope user` (use `--scope project` if this\nrule is in the project's .claude/rules directory). Report the result, including\nany conflict or manual-upgrade requirement. Do not delete edited files, bypass\npermissions or rerun login automatically. A new session loads updated instructions.\n";
3
- export declare const skill = "---\nname: openmerit-scout\ndescription: Consults OpenMerit Scout before choosing, adding, downloading, replacing or configuring any application inference model, including implicit choices while building features, pretrained browser/local models, SDK defaults, embeddings, OCR, speech, classification, detection, segmentation, LLMs and agents. Not for unrelated edits, ordinary inference calls or this coding agent's own model.\n---\n\n# OpenMerit \u00B7 Scout\n\nBefore introducing, recommending, replacing or configuring an application\ninference model, call the `scout_models` tool on the `openmerit` MCP server.\nThis applies even when the user never mentions models or Scout. Downloading local\nweights, using a pretrained browser model or accepting an SDK default is a model\ndecision: consult Scout before model-specific code, installs or downloads.\nPreserve the user's explicit model choice; assess it rather than silently replacing\nit. Leave unchanged, approved application selections in place.\n\n## Project policy\n\nRead `.openmerit/scout.json` before searching. If it is absent, ask once whether this\nproject uses **existing-only** (only endpoints it can already reach) or\n**existing-first** (prefer them, ask before searching the whole catalogue), and\nwhich public catalogue endpoint IDs it can access. Save the answer:\n\n```sh\nnpx -y openmerit@latest project --policy existing-only --endpoint-id <id> ...\n```\n\nNever infer access from a model name, SDK or environment-variable name. Never\nrecord credentials, private URLs, prompts or customer data. An empty inventory\nis not permission to search all models.\n\n## Searching\n\nCall `scout_models` with a non-confidential task description as `prompt` and\n`endpointIds` set to the saved inventory, including `[]` when empty. Send\nonly task requirements, never source files, secrets or real customer records.\nIf the MCP tool is unavailable, run\n`npx -y openmerit@latest scout \"<task>\" --json`; it applies the saved inventory.\n\nScout is a conversation with the agent behind openmerit.ai/scout. When\n`status` is `needs_answers`, fill in what the user or project already states,\nshow the user Scout's brief and remaining questions, and wait for their answers.\nThen call again with the returned `conversationId` and `brief` set to the\nrewritten components, or `skip: true` if the user wants Scout to assume. Never\nanswer Scout's questions with guesses. Send later refinements to the same\n`conversationId`. A call with only a `conversationId` reads an interrupted turn.\n\nWhen `status` is `recommended`, Scout has split the work into `tasks`, one\nper model, each with up to five ranked `models`; cover every task. Each\nmodel's `cost` names the `endpointId` and `providerModelId` to call.\nExisting-only never broadens. For existing-first, if no existing candidate fits a\ntask, explain the gap and ask before a new conversation without `endpointIds`.\nCompare quality evidence, reference cost/time and watch-outs. Prefer the cheapest\nusable option the evidence supports, without claiming benchmarks prove\napplication accuracy. Use the project's actual SDK and protocol.\n\n## Boundaries\n\nAsk before adding providers or credentials, spending, deploying or external\nwrites. Report recommendation, endpoint connectivity and application checks as\nseparate results, each only after it succeeds. Do not switch this coding agent's\nown model.\n\nIf Scout fails or has no suitable candidate/evidence for the required deployment,\nsay so and pause the model decision; do not substitute a remembered recommendation\nor SDK default. Continue only model-independent work. Local/browser models are not\nexempt when the catalogue lacks coverage. If it reports that the device key is missing,\nexpired or revoked, run `npx -y openmerit@latest login` and then\n`npx -y openmerit@latest install` again. Never ask the user to paste a key.\n";
4
- export declare function scoutRule(client: ScoutClient): string;
5
- export declare function scoutSkill(client: ScoutClient): string;