loopengine 0.1.95 → 0.1.97

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 (68) hide show
  1. package/README.md +21 -2
  2. package/dist/bin/ability-manager.d.ts +4 -0
  3. package/dist/bin/ability-manager.js +34 -2
  4. package/dist/bin/cli.d.ts +1 -1
  5. package/dist/bin/cli.js +3 -0
  6. package/dist/core/ability-details.d.ts +31 -0
  7. package/dist/core/ability-details.js +140 -0
  8. package/dist/core/ability-sources.d.ts +59 -0
  9. package/dist/core/ability-sources.js +312 -0
  10. package/dist/core/agent-config.d.ts +21 -1
  11. package/dist/core/discover-agents.js +11 -1
  12. package/dist/core/http-notifier.d.ts +7 -1
  13. package/dist/core/http-notifier.js +68 -9
  14. package/dist/core/http-notify-triggers/email.d.ts +2 -2
  15. package/dist/core/http-notify-triggers/email.js +2 -4
  16. package/dist/core/http-notify-triggers/lark.d.ts +2 -2
  17. package/dist/core/http-notify-triggers/lark.js +2 -4
  18. package/dist/core/http-notify-triggers/slack.d.ts +2 -2
  19. package/dist/core/http-notify-triggers/slack.js +2 -4
  20. package/dist/core/http-notify-triggers/telegram.d.ts +28 -0
  21. package/dist/core/http-notify-triggers/telegram.js +71 -0
  22. package/dist/core/http-notify-triggers/webhook.d.ts +2 -2
  23. package/dist/core/http-notify-triggers/webhook.js +2 -4
  24. package/dist/core/model-calls/meta-model-call.d.ts +15 -0
  25. package/dist/core/model-calls/meta-model-call.js +48 -0
  26. package/dist/core/model-calls/openai-model-call.d.ts +5 -0
  27. package/dist/core/model-calls/openai-model-call.js +47 -1
  28. package/dist/core/model-calls/xai-model-call.d.ts +15 -0
  29. package/dist/core/model-calls/xai-model-call.js +45 -0
  30. package/dist/core/notifier-file.d.ts +40 -6
  31. package/dist/core/notifier-file.js +119 -25
  32. package/dist/core/system-agents/workflow-planner.d.ts +100 -0
  33. package/dist/core/system-agents/workflow-planner.js +278 -0
  34. package/dist/core/workflows.d.ts +26 -0
  35. package/dist/core/workflows.js +39 -0
  36. package/dist/core/workspace-abilities.d.ts +31 -0
  37. package/dist/core/workspace-abilities.js +110 -0
  38. package/dist/index.d.ts +18 -4
  39. package/dist/index.js +17 -3
  40. package/dist/web/ability-details-admin.d.ts +17 -0
  41. package/dist/web/ability-details-admin.js +45 -0
  42. package/dist/web/ability-sources-admin.d.ts +33 -0
  43. package/dist/web/ability-sources-admin.js +65 -0
  44. package/dist/web/agent-file-admin.d.ts +1 -1
  45. package/dist/web/agent-file-admin.js +3 -0
  46. package/dist/web/agents-config-page.js +600 -220
  47. package/dist/web/agents-list-page.js +49 -20
  48. package/dist/web/console-shell.js +244 -3
  49. package/dist/web/dev-ui-styles.js +12 -0
  50. package/dist/web/global-config-page.js +348 -95
  51. package/dist/web/global-config.d.ts +44 -1
  52. package/dist/web/global-config.js +162 -2
  53. package/dist/web/interview-admin.js +2 -2
  54. package/dist/web/messaging-icons.d.ts +2 -0
  55. package/dist/web/messaging-icons.js +7 -0
  56. package/dist/web/notifier-admin.d.ts +33 -15
  57. package/dist/web/notifier-admin.js +109 -65
  58. package/dist/web/provider-icons.d.ts +2 -0
  59. package/dist/web/provider-icons.js +10 -0
  60. package/dist/web/templates-admin.d.ts +3 -0
  61. package/dist/web/templates-admin.js +5 -1
  62. package/dist/web/workflow-admin.d.ts +124 -0
  63. package/dist/web/workflow-admin.js +308 -0
  64. package/dist/web/workflow-page.d.ts +1 -0
  65. package/dist/web/workflow-page.js +226 -0
  66. package/dist/web/workspace-abilities-admin.d.ts +24 -0
  67. package/dist/web/workspace-abilities-admin.js +118 -0
  68. package/package.json +13 -1
package/README.md CHANGED
@@ -402,16 +402,35 @@ you, using the matching API key from the environment:
402
402
  | `'kimi'` | `MOONSHOT_API_KEY` | yes |
403
403
  | `'glm'` | `GLM_API_KEY` | yes |
404
404
  | `'gemini'` | `GEMINI_API_KEY` | yes |
405
+ | `'xai'` | `XAI_API_KEY` | yes |
406
+ | `'meta'` | `META_API_KEY` | yes |
405
407
 
406
408
  `'kimi'`'s env var is `MOONSHOT_API_KEY`, not `KIMI_API_KEY` — deliberate,
407
409
  matching Moonshot AI's own docs (the API/company behind Kimi) rather than
408
- this package's own provider name. `openai`/`deepseek`/`kimi`/`glm`/
409
- `gemini` all reuse the same OpenAI-Chat-Completions-compatible request
410
+ this package's own provider name. `'meta'` (Meta's Muse models, e.g.
411
+ `muse-spark-1.3`, through the Meta Model API) reads `META_API_KEY`,
412
+ LiteLLM's name, rather than the generic `MODEL_API_KEY` in Meta's own
413
+ examples. `openai`/`deepseek`/`kimi`/`glm`/`gemini`/`xai`/`meta` all reuse the same OpenAI-Chat-Completions-compatible request
410
414
  translation, just pointed at each provider's own base URL — see
411
415
  `core/model-calls/*.ts` for the provider-specific details (base URLs,
412
416
  `max_completion_tokens` vs `max_tokens`, and Gemini's own "still in beta"
413
417
  caveat on Google's compatibility layer).
414
418
 
419
+ The console's model dropdown (New agent, and an agent's Model > Edit)
420
+ lists each provider's models live from its API once its key is saved. To
421
+ choose the list yourself, add a `models.yaml` next to `.env` mapping each
422
+ provider to its models; a provider listed there offers exactly those, in
423
+ that order, with no key needed, and edits show the next time the
424
+ dropdown loads (`LOOPENGINE_MODELS_FILE` points at a different file):
425
+
426
+ ```yaml
427
+ anthropic:
428
+ - claude-opus-5-5
429
+ - claude-sonnet-5-5
430
+ deepseek:
431
+ - deepseek-v4-pro
432
+ ```
433
+
415
434
  For anything else (a custom SDK client, a canned/simulated model for
416
435
  testing), export your own `createModelCall(): ModelCall` instead.
417
436
 
@@ -133,6 +133,10 @@ export declare function backfillDependencies(agentName: string, abilityName: str
133
133
  * real network call is needed there; see ABILITIES.md's "Publishing an
134
134
  * ability" section). */
135
135
  export declare function fetchAbilityDir(spec: string): string;
136
+ /** fetchAbilityDir without blocking the server while npm and git run:
137
+ * for browsing (the console's ability details), where an install's
138
+ * stop-the-world fetch would freeze every other request. */
139
+ export declare function fetchAbilityDirAsync(spec: string): Promise<string>;
136
140
  export interface FetchOptions {
137
141
  fetchAbilityDir?: (spec: string) => string;
138
142
  /** Test-only override for the installing project's own "loopengine"
@@ -17,13 +17,15 @@
17
17
  // direction worth introducing to share ~30 lines (see ABILITIES.md's
18
18
  // "Upgrading" section).
19
19
  import { createHash } from 'node:crypto';
20
- import { execFileSync } from 'node:child_process';
20
+ import { execFile, execFileSync } from 'node:child_process';
21
+ import { promisify } from 'node:util';
21
22
  import { basename, dirname, join } from 'node:path';
22
23
  import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmdirSync, rmSync, writeFileSync } from 'node:fs';
23
24
  import { tmpdir } from 'node:os';
24
25
  import { parse as parseYaml } from 'yaml';
25
26
  import semver from 'semver';
26
27
  import { agentDir } from '../core/gateway-tools.js';
28
+ import { abilityFetchAuth } from '../core/ability-sources.js';
27
29
  import { addToolToIndex, removeToolFromIndex, toCamelCase } from '../web/http-tool-admin.js';
28
30
  import { addActauthRule, updateActauthRule, removeActauthRule, readActauthConfig } from '../web/actauth-admin.js';
29
31
  export class AbilityManifestError extends Error {
@@ -213,7 +215,15 @@ function sha256(content) {
213
215
  * ability" section). */
214
216
  export function fetchAbilityDir(spec) {
215
217
  const workDir = mkdtempSync(join(tmpdir(), 'loopengine-ability-'));
216
- execFileSync('npm', ['pack', spec, '--pack-destination', workDir], { stdio: 'pipe' });
218
+ // The private sources' tokens (core/ability-sources.ts), only for this
219
+ // npm pack: never in the spec, so they aren't written anywhere.
220
+ const auth = abilityFetchAuth();
221
+ try {
222
+ execFileSync('npm', ['pack', spec, '--pack-destination', workDir, ...auth.args], { stdio: 'pipe', env: auth.env });
223
+ }
224
+ finally {
225
+ auth.cleanup();
226
+ }
217
227
  const tarball = readdirSync(workDir).find((f) => f.endsWith('.tgz'));
218
228
  if (!tarball) {
219
229
  throw new Error(`Could not fetch '${spec}' — check the package exists and is reachable (registry auth / git credentials / local path).`);
@@ -225,6 +235,28 @@ export function fetchAbilityDir(spec) {
225
235
  execFileSync('tar', ['-xzf', join(workDir, tarball), '-C', workDir], { stdio: 'pipe' });
226
236
  return join(workDir, 'package');
227
237
  }
238
+ /** fetchAbilityDir without blocking the server while npm and git run:
239
+ * for browsing (the console's ability details), where an install's
240
+ * stop-the-world fetch would freeze every other request. */
241
+ export async function fetchAbilityDirAsync(spec) {
242
+ const workDir = mkdtempSync(join(tmpdir(), 'loopengine-ability-'));
243
+ const auth = abilityFetchAuth();
244
+ try {
245
+ await promisify(execFile)('npm', ['pack', spec, '--pack-destination', workDir, ...auth.args], { env: auth.env, timeout: 120_000 });
246
+ }
247
+ catch {
248
+ rmSync(workDir, { recursive: true, force: true });
249
+ throw new Error(`Could not fetch '${spec}' — check the package exists and is reachable (registry auth / git credentials / local path).`);
250
+ }
251
+ finally {
252
+ auth.cleanup();
253
+ }
254
+ const tarball = readdirSync(workDir).find((f) => f.endsWith('.tgz'));
255
+ if (!tarball)
256
+ throw new Error(`Could not fetch '${spec}'.`);
257
+ await promisify(execFile)('tar', ['-xzf', join(workDir, tarball), '-C', workDir]);
258
+ return join(workDir, 'package');
259
+ }
228
260
  function readManifest(abilityDir) {
229
261
  const manifestPath = join(abilityDir, 'loopengine.ability.json');
230
262
  if (!existsSync(manifestPath)) {
package/dist/bin/cli.d.ts CHANGED
@@ -7,7 +7,7 @@ export declare class AgentNotFoundError extends Error {
7
7
  }
8
8
  export declare class AgentModelError extends Error {
9
9
  }
10
- type Provider = 'anthropic' | 'openai' | 'deepseek' | 'kimi' | 'glm' | 'gemini';
10
+ type Provider = 'anthropic' | 'openai' | 'deepseek' | 'kimi' | 'glm' | 'gemini' | 'xai' | 'meta';
11
11
  export interface AgentTemplateOptions {
12
12
  systemPrompt?: string;
13
13
  model?: {
package/dist/bin/cli.js CHANGED
@@ -32,6 +32,9 @@ const MODEL_ENV_VAR = {
32
32
  kimi: 'MOONSHOT_API_KEY',
33
33
  glm: 'GLM_API_KEY',
34
34
  gemini: 'GEMINI_API_KEY',
35
+ xai: 'XAI_API_KEY',
36
+ // LiteLLM's name; see core/model-calls/meta-model-call.ts.
37
+ meta: 'META_API_KEY',
35
38
  };
36
39
  // Escapes a value for safe interpolation into a single-quoted TS string
37
40
  // literal in generated code — an operator-supplied systemPrompt (from
@@ -0,0 +1,31 @@
1
+ export type Decision = 'allow' | 'ask' | 'deny';
2
+ export interface AbilityToolDetails {
3
+ name: string;
4
+ description: string;
5
+ /** The decision the ability ships for it (its actauth rules), if any. */
6
+ decision?: Decision;
7
+ }
8
+ export interface AbilitySkillDetails {
9
+ name: string;
10
+ description: string;
11
+ }
12
+ export interface AbilityDetails {
13
+ name: string;
14
+ version: string;
15
+ description: string;
16
+ tools: AbilityToolDetails[];
17
+ skills: AbilitySkillDetails[];
18
+ /** Settings it reads (env var names), with a description when given. */
19
+ settings: Array<{
20
+ name: string;
21
+ description?: string;
22
+ }>;
23
+ }
24
+ /** Throws when `dir` holds no loopengine.ability.json. */
25
+ export declare function readAbilityDetails(dir: string): AbilityDetails;
26
+ /** The { name, description } of every object literal in a tool file that
27
+ * has both as string values: its ToolDefinition(s). Parsed, never run. */
28
+ export declare function readToolDefinitions(source: string, fileName?: string): Array<{
29
+ name: string;
30
+ description: string;
31
+ }>;
@@ -0,0 +1,140 @@
1
+ // What an ability can do, read from its files without running any of
2
+ // them: its tools (name and description, taken from each tool file's
3
+ // ToolDefinition with the TypeScript parser), the permission each tool
4
+ // ships with (its actauth rules), and its skills (each SKILL.md's
5
+ // frontmatter). Backs the console's ability details.
6
+ import { existsSync, readFileSync } from 'node:fs';
7
+ import { basename, join } from 'node:path';
8
+ import ts from 'typescript';
9
+ import { parse as parseYaml } from 'yaml';
10
+ /** Throws when `dir` holds no loopengine.ability.json. */
11
+ export function readAbilityDetails(dir) {
12
+ const manifestPath = join(dir, 'loopengine.ability.json');
13
+ if (!existsSync(manifestPath))
14
+ throw new Error('This package has no loopengine.ability.json, so it isn\'t a loopengine ability');
15
+ const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
16
+ let pkg = {};
17
+ try {
18
+ pkg = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8'));
19
+ }
20
+ catch { }
21
+ const decisions = readShippedDecisions(dir, manifest.actauth);
22
+ const tools = strings(manifest.tools).flatMap((file) => {
23
+ const path = join(dir, file);
24
+ const found = existsSync(path) ? readToolDefinitions(readFileSync(path, 'utf8'), file) : [];
25
+ // A file whose definition can't be read statically still shows, by its file name.
26
+ const listed = found.length ? found : [{ name: basename(file).replace(/\.[cm]?[jt]s$/, ''), description: '' }];
27
+ return listed.map((tool) => (decisions[tool.name] ? { ...tool, decision: decisions[tool.name] } : tool));
28
+ });
29
+ const skills = strings(manifest.skills).map((skillDir) => readSkill(join(dir, skillDir), skillDir));
30
+ return { name: pkg.name || basename(dir), version: pkg.version || '', description: pkg.description || '', tools, skills, settings: readSettings(manifest) };
31
+ }
32
+ function strings(value) {
33
+ return Array.isArray(value) ? value.filter((v) => typeof v === 'string') : [];
34
+ }
35
+ /** Each tool rule's decision, by tool name, from the ability's own rules
36
+ * file (a list of { tool, decision }, or { rules: [...] }). */
37
+ function readShippedDecisions(dir, actauth) {
38
+ if (typeof actauth !== 'string' || !existsSync(join(dir, actauth)))
39
+ return {};
40
+ let parsed;
41
+ try {
42
+ parsed = parseYaml(readFileSync(join(dir, actauth), 'utf8'));
43
+ }
44
+ catch {
45
+ return {};
46
+ }
47
+ const rules = Array.isArray(parsed) ? parsed : Array.isArray(parsed?.rules) ? parsed.rules : [];
48
+ const out = {};
49
+ for (const rule of rules) {
50
+ if (typeof rule?.tool === 'string' && (rule.decision === 'allow' || rule.decision === 'ask' || rule.decision === 'deny') && !out[rule.tool])
51
+ out[rule.tool] = rule.decision;
52
+ }
53
+ return out;
54
+ }
55
+ /** The { name, description } of every object literal in a tool file that
56
+ * has both as string values: its ToolDefinition(s). Parsed, never run. */
57
+ export function readToolDefinitions(source, fileName = 'tool.ts') {
58
+ const file = ts.createSourceFile(fileName, source, ts.ScriptTarget.Latest, true, fileName.endsWith('.js') ? ts.ScriptKind.JS : ts.ScriptKind.TS);
59
+ const found = [];
60
+ const visit = (node) => {
61
+ if (ts.isObjectLiteralExpression(node)) {
62
+ let name;
63
+ let description;
64
+ let hasSchema = false;
65
+ for (const prop of node.properties) {
66
+ if (!ts.isPropertyAssignment(prop))
67
+ continue;
68
+ const key = ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name) ? prop.name.text : undefined;
69
+ if (key === 'name')
70
+ name = staticString(prop.initializer);
71
+ else if (key === 'description')
72
+ description = staticString(prop.initializer);
73
+ else if (key === 'input_schema' || key === 'inputSchema' || key === 'execute')
74
+ hasSchema = true;
75
+ }
76
+ // A tool definition, not an input-schema property that also has a
77
+ // description: those have no name.
78
+ if (name && description !== undefined && hasSchema) {
79
+ found.push({ name, description });
80
+ return;
81
+ }
82
+ }
83
+ ts.forEachChild(node, visit);
84
+ };
85
+ visit(file);
86
+ return found;
87
+ }
88
+ /** A string literal, a template without substitutions, or a + of them. */
89
+ function staticString(node) {
90
+ if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node))
91
+ return node.text;
92
+ if (ts.isParenthesizedExpression(node))
93
+ return staticString(node.expression);
94
+ if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.PlusToken) {
95
+ const left = staticString(node.left);
96
+ const right = staticString(node.right);
97
+ return left !== undefined && right !== undefined ? left + right : undefined;
98
+ }
99
+ return undefined;
100
+ }
101
+ function readSkill(dir, skillDir) {
102
+ const fallback = { name: basename(skillDir), description: '' };
103
+ const path = join(dir, 'SKILL.md');
104
+ if (!existsSync(path))
105
+ return fallback;
106
+ const match = readFileSync(path, 'utf8').match(/^---\r?\n([\s\S]*?)\r?\n---/);
107
+ if (!match)
108
+ return fallback;
109
+ try {
110
+ const front = parseYaml(match[1]);
111
+ return { name: typeof front?.name === 'string' ? front.name : fallback.name, description: typeof front?.description === 'string' ? front.description : '' };
112
+ }
113
+ catch {
114
+ return fallback;
115
+ }
116
+ }
117
+ /** The env vars an ability declares it reads, from loopengine.ability.json
118
+ * (`env` or `settings`: a list of names or { name, description }, or a
119
+ * map of name to description or { description }). */
120
+ function readSettings(manifest) {
121
+ const raw = manifest.env ?? manifest.settings;
122
+ const out = [];
123
+ const add = (name, description) => {
124
+ if (typeof name === 'string' && name && !out.some((s) => s.name === name))
125
+ out.push(typeof description === 'string' && description ? { name, description } : { name });
126
+ };
127
+ if (Array.isArray(raw)) {
128
+ for (const entry of raw) {
129
+ if (typeof entry === 'string')
130
+ add(entry);
131
+ else if (entry && typeof entry === 'object')
132
+ add(entry.name, entry.description);
133
+ }
134
+ }
135
+ else if (raw && typeof raw === 'object') {
136
+ for (const [name, value] of Object.entries(raw))
137
+ add(name, typeof value === 'string' ? value : value?.description);
138
+ }
139
+ return out;
140
+ }
@@ -0,0 +1,59 @@
1
+ export type AbilitySource = {
2
+ id: string;
3
+ type: 'github';
4
+ owner: string;
5
+ } | {
6
+ id: string;
7
+ type: 'npm';
8
+ registry: string;
9
+ scope: string;
10
+ } | {
11
+ id: string;
12
+ type: 'folder';
13
+ path: string;
14
+ };
15
+ export interface SourceAbility {
16
+ name: string;
17
+ description: string;
18
+ version: string;
19
+ /** What to install it with: github:owner/repo, @scope/name or file:/path. */
20
+ spec: string;
21
+ }
22
+ export interface AbilityOrigin {
23
+ kind: 'public' | 'private';
24
+ /** Where it came from, for the console's badge: "npm", "GitHub
25
+ * loopengine-co/lp-github", "Folder ../loopengine-abilities", ... */
26
+ label: string;
27
+ }
28
+ export declare class AbilitySourceError extends Error {
29
+ }
30
+ export declare const ABILITY_SOURCES_FILE_ENV = "LOOPENGINE_ABILITY_SOURCES_FILE";
31
+ export declare function abilitySourcesPath(): string;
32
+ /** The env var a source's token lives in. */
33
+ export declare function sourceTokenVar(id: string): string;
34
+ /** Throws AbilitySourceError naming the first problem. An id is made up
35
+ * from the source when none is given. */
36
+ export declare function validateAbilitySource(input: unknown): AbilitySource;
37
+ export declare function readAbilitySources(): AbilitySource[];
38
+ /** Adds a source, or replaces the one with the same id. */
39
+ export declare function saveAbilitySource(input: unknown): AbilitySource;
40
+ export declare function removeAbilitySource(id: string): void;
41
+ export declare function describeSource(source: AbilitySource): string;
42
+ /** The abilities a source offers. GitHub and npm listings are cached for
43
+ * five minutes per token; a folder is read fresh. Throws with a message
44
+ * fit for the console when the source can't be read. */
45
+ export declare function listSourceAbilities(source: AbilitySource, options?: {
46
+ fetch?: typeof fetch;
47
+ }): Promise<SourceAbility[]>;
48
+ /** Public (npm) or private (a source, a git repo or a local folder), from
49
+ * the spec it was installed with. */
50
+ export declare function abilityOrigin(spec: string | undefined, name: string, sources?: AbilitySource[]): AbilityOrigin;
51
+ /** What `npm pack` needs to fetch from the private sources: GitHub tokens
52
+ * as git URL rewrites (only in the child's env, per owner), and a
53
+ * temporary npm config adding each private scope's registry and token to
54
+ * the user's own. `cleanup` removes that file. */
55
+ export declare function abilityFetchAuth(sources?: AbilitySource[]): {
56
+ args: string[];
57
+ env: NodeJS.ProcessEnv;
58
+ cleanup: () => void;
59
+ };