@agentprojectcontext/apx 1.8.2 → 1.10.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.
@@ -1,4 +1,12 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
1
3
  import { readAgents } from "../../../core/parser.js";
4
+ import {
5
+ buildApfHint,
6
+ closeRuntimeSession,
7
+ createRuntimeSession,
8
+ extractApfResult,
9
+ } from "../../apc-runtime-context.js";
2
10
  import { getRuntime, RUNTIME_IDS } from "../../runtimes/index.js";
3
11
  import { buildAgentSystem, confirmedProperty, resolveProject } from "../helpers.js";
4
12
 
@@ -20,37 +28,70 @@ function resolveProjectForAgent(projects, project, slug) {
20
28
  return resolveProject(projects, project);
21
29
  }
22
30
 
31
+ function projectName(project) {
32
+ try {
33
+ const meta = JSON.parse(fs.readFileSync(path.join(project.path, ".apc", "project.json"), "utf8"));
34
+ if (meta.name) return meta.name;
35
+ } catch {}
36
+ return path.basename(project.path);
37
+ }
38
+
39
+ function buildRuntimeSystem(project, agent, runtime, sessionId, caller) {
40
+ const agentSlug = agent?.slug || "apx";
41
+ const hint = buildApfHint({
42
+ projectName: projectName(project),
43
+ projectPath: project.path,
44
+ agentSlug,
45
+ sessionId,
46
+ });
47
+ if (agent) {
48
+ return buildAgentSystem(project, agent, {
49
+ invocation: "runtime",
50
+ runtime,
51
+ caller,
52
+ extraParts: [hint],
53
+ });
54
+ }
55
+
56
+ return [
57
+ "You are APX running inside an external coding runtime.",
58
+ "No APC agent was explicitly selected for this run.",
59
+ "Use the project context and runtime tools directly. Do not impersonate a project agent.",
60
+ hint,
61
+ ].join("\n\n");
62
+ }
63
+
23
64
  export default {
24
65
  name: "call_runtime",
25
66
  schema: {
26
67
  type: "function",
27
68
  function: {
28
69
  name: "call_runtime",
29
- description: "Spawn an external CLI runtime (Claude Code, Codex, OpenCode, Aider) impersonating an APC agent.",
70
+ description: "Spawn an external CLI runtime (Claude Code, Codex, OpenCode, Aider, Cursor Agent, Gemini CLI, Qwen Code), optionally impersonating an APC agent.",
30
71
  parameters: {
31
72
  type: "object",
32
73
  properties: {
33
74
  project: { type: "string" },
34
- agent: { type: "string", description: "APC agent slug from AGENTS.md, not runtime name" },
75
+ agent: { type: "string", description: "Optional APC agent slug from AGENTS.md, not runtime name. Omit when the user did not name an agent." },
35
76
  runtime: {
36
77
  type: "string",
37
- enum: ["claude-code", "codex", "opencode", "aider"],
78
+ enum: RUNTIME_IDS,
38
79
  description: "external CLI runtime",
39
80
  },
40
81
  prompt: { type: "string" },
41
82
  timeout_s: { type: "integer", description: "seconds before SIGTERM; default 300" },
42
83
  confirmed: confirmedProperty("true only after explicit user confirmation for this exact runtime command"),
43
84
  },
44
- required: ["agent", "runtime", "prompt"],
85
+ required: ["runtime", "prompt"],
45
86
  },
46
87
  },
47
88
  },
48
89
  makeHandler: ({ projects, requirePermission }) => async ({ project, agent: slug, runtime, prompt, timeout_s = 300, confirmed = false }) => {
49
90
  requirePermission("call_runtime", { dangerous: true, confirmed });
50
91
 
51
- const p = resolveProjectForAgent(projects, project, slug);
52
- const agent = readAgents(p.path).find((a) => a.slug === slug);
53
- if (!agent) {
92
+ const p = slug ? resolveProjectForAgent(projects, project, slug) : resolveProject(projects, project);
93
+ const agent = slug ? readAgents(p.path).find((a) => a.slug === slug) : null;
94
+ if (slug && !agent) {
54
95
  const directory = projects.list().map((entry) => ({
55
96
  project: entry.name,
56
97
  kind: entry.id === 0 ? "default" : "project",
@@ -67,45 +108,74 @@ export default {
67
108
  return { error: `${e.message}. Available runtimes: ${RUNTIME_IDS.join(", ")}` };
68
109
  }
69
110
 
70
- const r = await rt.run({
71
- system: buildAgentSystem(p, agent, {
72
- invocation: "runtime",
73
- runtime,
74
- caller: "super_agent_tool",
75
- }),
76
- prompt,
77
- cwd: p.path,
78
- timeoutMs: timeout_s * 1000,
111
+ const actor = agent?.slug || "apx";
112
+ const session = createRuntimeSession({
113
+ projectRoot: p.path,
114
+ storageRoot: p.storagePath,
115
+ agentSlug: actor,
116
+ runtime,
117
+ title: `Runtime: ${runtime}${agent ? ` (${agent.slug})` : ""}`,
79
118
  });
80
119
 
81
- p.logMessage({
82
- agent_slug: slug,
83
- channel: "runtime",
84
- direction: "in",
85
- author: "user",
86
- body: prompt,
87
- meta: { runtime, invoked_by: "super_agent_tool" },
88
- });
89
- p.logMessage({
90
- agent_slug: slug,
91
- channel: "runtime",
92
- direction: "out",
93
- author: slug,
94
- body: r.output || "",
95
- meta: {
120
+ try {
121
+ const r = await rt.run({
122
+ system: buildRuntimeSystem(p, agent, runtime, session.id, "super_agent_tool"),
123
+ prompt,
124
+ cwd: p.path,
125
+ timeoutMs: timeout_s * 1000,
126
+ });
127
+
128
+ const result = extractApfResult(r.output) || (r.output || "").slice(0, 200);
129
+ closeRuntimeSession({
130
+ filePath: session.path,
131
+ externalSessionPath: r.externalSessionPath || null,
132
+ exitCode: r.exitCode,
133
+ result,
134
+ });
135
+
136
+ p.logMessage({
137
+ agent_slug: actor,
138
+ channel: "runtime",
139
+ direction: "in",
140
+ author: "user",
141
+ body: prompt,
142
+ meta: { runtime, invoked_by: "super_agent_tool", apc_session: session.id },
143
+ });
144
+ p.logMessage({
145
+ agent_slug: actor,
146
+ channel: "runtime",
147
+ direction: "out",
148
+ author: actor,
149
+ body: r.output || "",
150
+ meta: {
151
+ runtime,
152
+ exit_code: r.exitCode,
153
+ external_session_path: r.externalSessionPath || null,
154
+ session_id: r.sessionId || null,
155
+ apc_session: session.id,
156
+ invoked_by: "super_agent_tool",
157
+ },
158
+ });
159
+
160
+ return {
96
161
  runtime,
162
+ agent: agent?.slug || null,
163
+ apc_session: session.id,
97
164
  exit_code: r.exitCode,
165
+ output: (r.output || "").slice(0, 4000),
166
+ truncated: (r.output || "").length > 4000,
98
167
  external_session_path: r.externalSessionPath || null,
99
- invoked_by: "super_agent_tool",
100
- },
101
- });
102
-
103
- return {
104
- runtime,
105
- exit_code: r.exitCode,
106
- output: (r.output || "").slice(0, 4000),
107
- truncated: (r.output || "").length > 4000,
108
- external_session_path: r.externalSessionPath || null,
109
- };
168
+ session_id: r.sessionId || null,
169
+ };
170
+ } catch (e) {
171
+ try {
172
+ closeRuntimeSession({
173
+ filePath: session.path,
174
+ exitCode: -1,
175
+ result: `error: ${e.message.slice(0, 200)}`,
176
+ });
177
+ } catch {}
178
+ throw e;
179
+ }
110
180
  },
111
181
  };
@@ -0,0 +1,66 @@
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ import { resolveProject, safePathJoin } from "../helpers.js";
4
+
5
+ const execFileAsync = promisify(execFile);
6
+
7
+ export default {
8
+ name: "search_files",
9
+ schema: {
10
+ type: "function",
11
+ function: {
12
+ name: "search_files",
13
+ description: "Search for text patterns inside project files using ripgrep or grep.",
14
+ parameters: {
15
+ type: "object",
16
+ properties: {
17
+ query: { type: "string", description: "The text or regex pattern to search for." },
18
+ project: { type: "string" },
19
+ path: { type: "string", description: "relative path inside the project to restrict search; default '.'" },
20
+ },
21
+ required: ["query"],
22
+ },
23
+ },
24
+ },
25
+ makeHandler: ({ projects, requirePermission }) => async ({ query, project, path: sub = "." } = {}) => {
26
+ // Optional permission check if it's considered destructive, but search is safe read-only
27
+ await requirePermission("search_files", { query, project, path: sub }, "safe");
28
+
29
+ const p = resolveProject(projects, project);
30
+ const target = safePathJoin(p.path, sub);
31
+
32
+ try {
33
+ const { stdout } = await execFileAsync("rg", ["-n", "--no-heading", "--color=never", query, target], {
34
+ cwd: p.path,
35
+ maxBuffer: 5 * 1024 * 1024,
36
+ });
37
+ return formatResults(stdout);
38
+ } catch (e) {
39
+ if (e.code === "ENOENT" || e.message.includes("ENOENT")) {
40
+ try {
41
+ const { stdout } = await execFileAsync("grep", ["-rn", query, target], {
42
+ cwd: p.path,
43
+ maxBuffer: 5 * 1024 * 1024,
44
+ });
45
+ return formatResults(stdout);
46
+ } catch (e2) {
47
+ if (e2.code === 1) return { result: "No matches found." };
48
+ throw new Error(`grep failed: ${e2.message}`);
49
+ }
50
+ }
51
+ if (e.code === 1) return { result: "No matches found." };
52
+ return { error: `search failed: ${e.message}` };
53
+ }
54
+ },
55
+ };
56
+
57
+ function formatResults(stdout) {
58
+ if (!stdout) return { result: "No matches found." };
59
+ const lines = stdout.split('\n').slice(0, 100);
60
+ const out = lines.join('\n');
61
+ if (lines.length >= 100) {
62
+ return { result: out + '\n...(truncated)' };
63
+ }
64
+ return { result: out };
65
+ }
66
+
@@ -34,6 +34,7 @@ Useful CLI facts:
34
34
  - Permission mode: apx permission show; apx permission set total|automatico|permiso.
35
35
  - Routines: apx routine list|get|history|run|add. Autonomous super-agent routines use kind super_agent.
36
36
  - Routine design: if the user asks for an agent to think, decide, write, or reply, create an exec_agent routine with spec.agent and spec.prompt. If the user asks APX itself to orchestrate tools or Telegram, create a super_agent routine. If the request is only a deterministic command, create a shell routine. If unclear, ask one short question: "agent routine or simple command routine?"
37
+ - Routine schedules: APX supports standard cron expressions (e.g. '*/5 * * * *'), OR 'every:<number><s|m|h|d>' (e.g. 'every:60s'), OR 'once:<iso-8601>'.
37
38
  - Safe read-only shell checks such as apx --help, apx routine list, docker ps, find, ls, rg, grep can run in automatico without asking.
38
39
 
39
40
  Channel context:
@@ -42,20 +43,6 @@ Channel context:
42
43
 
43
44
  You HAVE tools. THE FIRST THING you do for any factual question is call a tool. Do not ask the user to specify a project unless the tool itself fails.
44
45
 
45
- Available tools:
46
- - list_projects, list_agents, list_mcps — discovery (call WITHOUT project to get all of them across every registered project; specify project only to filter)
47
- - list_vault_agents, import_agent, add_project — inspect the agent vault, install a vault agent into a project, register an APC project
48
- - read_agent_memory — what an agent knows
49
- - list_files, read_file, write_file, edit_file — inspect/create/edit files in default or a project
50
- - run_shell — execute shell commands in default or a project
51
- - tail_messages, search_messages — see history
52
- - call_agent — delegate to a project agent
53
- - call_mcp — call an installed MCP tool when MCP is the right protocol
54
- - call_runtime — spawn a separate claude-code/codex/opencode/aider session when the user wants an external runtime/chat
55
- - send_telegram — send a message
56
- - set_identity — update agent name, personality, owner, language (persists to disk)
57
- - set_permission_mode — set total/automatico/permiso in ~/.apx/config.json
58
-
59
46
  HARD RULES (do not deviate):
60
47
  1. NEVER invent project names, agent slugs, model ids, MCP names or paths. ALWAYS look them up via list_* first.
61
48
  2. If the user asks for agents, lists, inventory, or "what exists" without specifying a project, that means **all of them** — call the tool WITHOUT a project argument and the result will include every project.
@@ -66,8 +53,8 @@ HARD RULES (do not deviate):
66
53
  7. Stay brief: under 6 sentences unless asked for detail.
67
54
  8. You DO see recent prior turns of this chat as previous messages when applicable. **Use them ONLY to disambiguate references** (e.g. "el primero" → first project mentioned earlier). For ANY factual data — agent details, MCP details, file contents, memory — RE-CALL the tool. Past turns are context, not a cache. Models change, agents change, files change.
68
55
  9. /reset or /new from the user means "forget previous turns and answer this one fresh" — if you see those prefixes the operator already cleared the context for you.
69
- 10. ACTION RULE: use direct tools for direct work. run_shell executes commands; write_file/edit_file modify files. call_runtime is only for spawning a separate external runtime/chat. call_mcp is only for an MCP server/tool.
70
- 11. DISPATCH RULE: when the user asks a named agent to work inside Claude, Codex, OpenCode, or Aider, that is a call_runtime request. Look up the agent slug with list_agents if needed, then call call_runtime({agent: <slug>, runtime: 'claude-code'|'codex'|'opencode'|'aider', prompt: <user's request>}). The agent's declared model (in AGENTS.md) is IGNORED in this case; the runtime supplies the model. Memory + skills of the agent become the system prompt of the runtime.
56
+ 10. DELEGATION RULE: when the user asks an agent to do a task, use call_agent (unless they specify opening it in a runtime, then see rule 11).
57
+ 11. DISPATCH RULE: when the user asks to work inside Claude, Codex, OpenCode, Aider, Cursor, Gemini CLI, or Qwen Code, use call_runtime({runtime: 'claude-code'|'codex'|'opencode'|'aider'|'cursor-agent'|'gemini-cli'|'qwen-code', prompt: <user's request>}). If they explicitly name an agent to spawn, pass agent: <slug>. If they don't name an agent, DO NOT pass an agent argument. When an agent is passed, its memory + skills become the system prompt of the runtime.
71
58
  12. PROJECT RULE: when the user gives no project, use project "default". Do not infer a non-default project from old chat history unless the user references it. If they mention a path or project name, look it up or add it with add_project.
72
59
  13. VAULT RULE: when the user wants a new existing agent/template, call list_vault_agents first. If a suitable vault agent exists, import_agent into the chosen project. If none fits, say briefly what is missing.
73
60
  14. NO-PENDING RULE: never say "give me a second", "I will do it", or "I will try later" as a final answer. Either call the tool in this same turn or say what blocks you.
@@ -99,11 +86,13 @@ export async function runSuperAgent({
99
86
  prompt,
100
87
  contextNote = "",
101
88
  previousMessages = [],
89
+ overrideModel = null,
102
90
  }) {
103
91
  if (!isSuperAgentEnabled(globalConfig)) {
104
92
  throw new Error("super-agent not enabled (set super_agent.enabled and .model in ~/.apx/config.json)");
105
93
  }
106
94
  const sa = globalConfig.super_agent;
95
+ const activeModel = overrideModel || sa.model;
107
96
 
108
97
  // Tiny project hint — JUST names + ids, no detail. The model is expected to
109
98
  // call list_agents / list_mcps / read_agent_memory / etc. for everything
@@ -153,7 +142,7 @@ export async function runSuperAgent({
153
142
 
154
143
  for (let iter = 0; iter < MAX_TOOL_ITERS; iter++) {
155
144
  const result = await callEngine({
156
- modelId: sa.model,
145
+ modelId: activeModel,
157
146
  system,
158
147
  messages: conversation,
159
148
  config: globalConfig,
package/src/mcp/index.js CHANGED
@@ -95,7 +95,7 @@ server.tool(
95
95
  {
96
96
  slug: z.string().describe("Agent slug"),
97
97
  prompt: z.string().describe("Prompt / task for the agent"),
98
- runtime: z.string().optional().describe("Runtime: claude-code | codex (default: claude-code)"),
98
+ runtime: z.string().optional().describe("Runtime: claude-code | codex | opencode | aider | cursor-agent | gemini-cli | qwen-code (default: claude-code)"),
99
99
  },
100
100
  async ({ slug, prompt, runtime }) => {
101
101
  const proj = await resolveProject();