@apolloyh/apollo-agent 0.1.4 → 0.1.6

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/README.md CHANGED
@@ -35,9 +35,17 @@ apollo -r <session-id>
35
35
 
36
36
  Apollo loads project `.apollo/.env`, project `.env`, then user-level `~/.apollo/.env`; use `--env-file <path>` to override discovery.
37
37
 
38
- Optional project configuration lives at `.apollo/config.json`; Apollo also reads the legacy
39
- `agent.config.json` during migration. Copy `.apollo/config.example.json` to get started.
40
- Set `systemPrompt` there to append stable custom system instructions; restart Apollo to reload them.
38
+ Apollo creates global defaults at `~/.apollo/config.json`. Project overrides live at
39
+ `.apollo/config.json`; Apollo also reads the legacy `agent.config.json` during migration.
40
+ Configuration layers as built-in defaults user config project config `--config`.
41
+ Set `systemPrompt` in either config to append stable custom system instructions; restart Apollo to reload them.
42
+
43
+ Apollo also loads `~/.apollo/APOLLO.md` followed by the project `APOLLO.md` (lowercase
44
+ `apollo.md` is accepted). If no project Apollo file exists, the standard `AGENTS.md` and
45
+ `.apollo/instructions.md` paths remain supported. These instructions are frozen for the session.
46
+
47
+ While Apollo is working, type and press Enter to queue another user message. Queued messages
48
+ run in FIFO order after the current turn; press Esc to interrupt the current turn safely.
41
49
 
42
50
  Commands: `/help` · `/skills` · `/agents` · `/sessions` · `/resume` · `/verbose` · `/stream` · `/clear` · `/exit`
43
51
 
package/dist/brand.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /** Product identity — single source of truth for display strings. */
2
2
  export declare const PRODUCT: {
3
3
  readonly name: "Apollo";
4
- readonly version: "0.1.3";
4
+ readonly version: "0.1.6";
5
5
  readonly tagline: "Your personal agent — answer first, act when needed.";
6
6
  readonly packageName: "@apolloyh/apollo-agent";
7
7
  readonly userAgent: "ApolloAgent/0.1";
package/dist/brand.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /** Product identity — single source of truth for display strings. */
2
2
  export const PRODUCT = {
3
3
  name: "Apollo",
4
- version: "0.1.3",
4
+ version: "0.1.6",
5
5
  tagline: "Your personal agent — answer first, act when needed.",
6
6
  packageName: "@apolloyh/apollo-agent",
7
7
  userAgent: "ApolloAgent/0.1",
@@ -0,0 +1,16 @@
1
+ export type PastedTextBlock = {
2
+ id: number;
3
+ label: string;
4
+ marker: string;
5
+ text: string;
6
+ };
7
+ export type InputView = {
8
+ cursorLine: number;
9
+ cursorWidth: number;
10
+ lines: string[];
11
+ };
12
+ export declare function createPastedTextBlock(text: string, id: number): PastedTextBlock | undefined;
13
+ export declare function resolvePastedText(value: string, blocks: ReadonlyMap<string, PastedTextBlock>): string;
14
+ export declare function pastedTextPreview(value: string, blocks: ReadonlyMap<string, PastedTextBlock>): string;
15
+ export declare function renderInputView(value: string, cursor: number, width: number, blocks: ReadonlyMap<string, PastedTextBlock>): InputView;
16
+ //# sourceMappingURL=cli-input.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli-input.d.ts","sourceRoot":"","sources":["../src/cli-input.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,eAAe,GAAG;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB,CAAC;AAMF,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,eAAe,GAAG,SAAS,CAQ3F;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,CAAC,MAAM,EAAE,eAAe,CAAC,GAAG,MAAM,CAErG;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,CAAC,MAAM,EAAE,eAAe,CAAC,GAAG,MAAM,CAErG;AAED,wBAAgB,eAAe,CAC7B,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,WAAW,CAAC,MAAM,EAAE,eAAe,CAAC,GAC3C,SAAS,CAyCX"}
@@ -0,0 +1,68 @@
1
+ const MIN_FOLDED_PASTE_LINES = 6;
2
+ const MIN_FOLDED_PASTE_CHARS = 1000;
3
+ const FIRST_MARKER = 0xf0000;
4
+ export function createPastedTextBlock(text, id) {
5
+ const lineCount = text.split("\n").length;
6
+ if (lineCount < MIN_FOLDED_PASTE_LINES && Array.from(text).length < MIN_FOLDED_PASTE_CHARS) {
7
+ return undefined;
8
+ }
9
+ const marker = String.fromCodePoint(FIRST_MARKER + id);
10
+ const size = lineCount > 1 ? `+${lineCount} lines` : `+${Array.from(text).length} chars`;
11
+ return { id, label: `[Pasted text #${id} ${size}]`, marker, text };
12
+ }
13
+ export function resolvePastedText(value, blocks) {
14
+ return Array.from(value, (character) => blocks.get(character)?.text ?? character).join("");
15
+ }
16
+ export function pastedTextPreview(value, blocks) {
17
+ return Array.from(value, (character) => blocks.get(character)?.label ?? character).join("");
18
+ }
19
+ export function renderInputView(value, cursor, width, blocks) {
20
+ const characters = Array.from(value);
21
+ const positions = [];
22
+ const lines = [""];
23
+ let line = 0;
24
+ let column = 0;
25
+ const newLine = () => {
26
+ lines.push("");
27
+ line += 1;
28
+ column = 0;
29
+ };
30
+ const appendCharacter = (character) => {
31
+ if (character === "\n") {
32
+ newLine();
33
+ return;
34
+ }
35
+ const rendered = character === "\t" ? " " : character;
36
+ for (const outputCharacter of rendered) {
37
+ const characterWidth = displayWidth(outputCharacter);
38
+ if (column > 0 && column + characterWidth > width)
39
+ newLine();
40
+ lines[line] += outputCharacter;
41
+ column += characterWidth;
42
+ }
43
+ };
44
+ characters.forEach((character, index) => {
45
+ const block = blocks.get(character);
46
+ if (block && column > 0 && column + displayWidth(block.label) > width)
47
+ newLine();
48
+ positions[index] = { line, width: column };
49
+ for (const outputCharacter of block?.label ?? character)
50
+ appendCharacter(outputCharacter);
51
+ });
52
+ positions[characters.length] = { line, width: column };
53
+ const cursorPosition = positions[Math.max(0, Math.min(cursor, characters.length))] ?? { line: 0, width: 0 };
54
+ return {
55
+ cursorLine: cursorPosition.line,
56
+ cursorWidth: cursorPosition.width,
57
+ lines,
58
+ };
59
+ }
60
+ function displayWidth(text) {
61
+ let length = 0;
62
+ for (const character of text)
63
+ length += isWideCharacter(character) ? 2 : 1;
64
+ return length;
65
+ }
66
+ function isWideCharacter(character) {
67
+ return /[\u1100-\u115F\u2E80-\uA4CF\uAC00-\uD7A3\uF900-\uFAFF\uFE10-\uFE19\uFE30-\uFE6F\uFF00-\uFF60\uFFE0-\uFFE6]/u.test(character);
68
+ }
package/dist/config.d.ts CHANGED
@@ -6,6 +6,10 @@ export declare function ensureUserEnvFile(): {
6
6
  path: string;
7
7
  created: boolean;
8
8
  };
9
+ export declare function ensureUserConfigFile(): {
10
+ path: string;
11
+ created: boolean;
12
+ };
9
13
  export declare function loadAgentConfig(configPath?: string): Promise<AgentConfig>;
10
14
  /**
11
15
  * Load an explicit env file, or discover project env then ~/.apollo/.env.
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEzE,wDAAwD;AACxD,eAAO,MAAM,cAAc,EAAE,cAAc,EA4B1C,CAAC;AAsBF,eAAO,MAAM,0BAA0B,oHAC4E,CAAC;AAWpH,wBAAgB,iBAAiB,IAAI;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAatE;AAED,wBAAsB,eAAe,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CA0B/E;AAiBD;;;GAGG;AACH,wBAAgB,WAAW,CAAC,OAAO,CAAC,EAAE,MAAM,EAAE,OAAO,GAAE;IAAE,QAAQ,CAAC,EAAE,OAAO,CAAA;CAAO,GAAG,OAAO,CAwB3F;AAYD,wBAAgB,aAAa,CAAC,OAAO,GAAE;IAAE,gBAAgB,CAAC,EAAE,OAAO,CAAA;CAAO,GAAG,SAAS,CAkCrF"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEzE,wDAAwD;AACxD,eAAO,MAAM,cAAc,EAAE,cAAc,EA4B1C,CAAC;AAsBF,eAAO,MAAM,0BAA0B,oHAC4E,CAAC;AAoBpH,wBAAgB,iBAAiB,IAAI;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAatE;AAED,wBAAgB,oBAAoB,IAAI;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAazE;AAED,wBAAsB,eAAe,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAkD/E;AAiBD;;;GAGG;AACH,wBAAgB,WAAW,CAAC,OAAO,CAAC,EAAE,MAAM,EAAE,OAAO,GAAE;IAAE,QAAQ,CAAC,EAAE,OAAO,CAAA;CAAO,GAAG,OAAO,CAwB3F;AAYD,wBAAgB,aAAa,CAAC,OAAO,GAAE;IAAE,gBAAgB,CAAC,EAAE,OAAO,CAAA;CAAO,GAAG,SAAS,CAkCrF"}
package/dist/config.js CHANGED
@@ -57,6 +57,14 @@ ANTHROPIC_AUTH_TOKEN=
57
57
  # ANTHROPIC_BASE_URL=https://api.anthropic.com
58
58
  # ANTHROPIC_MODEL=claude-haiku-4-5
59
59
  `;
60
+ const USER_CONFIG_TEMPLATE = `${JSON.stringify({
61
+ systemPrompt: "",
62
+ maxTurns: defaultConfig.maxTurns,
63
+ context: defaultConfig.context,
64
+ permissions: defaultConfig.permissions,
65
+ skills: defaultConfig.skills,
66
+ mcpServers: defaultConfig.mcpServers,
67
+ }, null, 2)}\n`;
60
68
  export function ensureUserEnvFile() {
61
69
  const directory = path.join(os.homedir(), ".apollo");
62
70
  const envPath = path.join(directory, ".env");
@@ -73,27 +81,69 @@ export function ensureUserEnvFile() {
73
81
  throw error;
74
82
  }
75
83
  }
84
+ export function ensureUserConfigFile() {
85
+ const directory = path.join(os.homedir(), ".apollo");
86
+ const configPath = path.join(directory, "config.json");
87
+ if (fsSync.existsSync(configPath))
88
+ return { path: configPath, created: false };
89
+ fsSync.mkdirSync(directory, { recursive: true, mode: 0o700 });
90
+ try {
91
+ fsSync.writeFileSync(configPath, USER_CONFIG_TEMPLATE, { encoding: "utf8", flag: "wx", mode: 0o600 });
92
+ return { path: configPath, created: true };
93
+ }
94
+ catch (error) {
95
+ if (error.code === "EEXIST")
96
+ return { path: configPath, created: false };
97
+ throw error;
98
+ }
99
+ }
76
100
  export async function loadAgentConfig(configPath) {
77
- const discoveredPath = configPath ?? [DEFAULT_CONFIG_PATH, LEGACY_CONFIG_PATH].find(fsSync.existsSync);
78
- if (!discoveredPath) {
79
- return { ...defaultConfig, workspaceRoot: path.resolve(defaultConfig.workspaceRoot) };
80
- }
81
- const raw = await fs.readFile(discoveredPath, "utf8");
82
- const parsed = JSON.parse(raw);
83
- const resolvedConfigPath = path.resolve(discoveredPath);
84
- const configDir = path.dirname(resolvedConfigPath);
85
- const baseDir = path.basename(configDir) === ".apollo" ? path.dirname(configDir) : configDir;
86
- const agents = normalizeAgents(parsed.agents ?? parsed.workers) ?? defaultConfig.agents;
87
- const config = {
101
+ const userConfigPath = path.join(os.homedir(), ".apollo", "config.json");
102
+ const projectConfigPath = [DEFAULT_CONFIG_PATH, LEGACY_CONFIG_PATH].find(fsSync.existsSync);
103
+ const layers = [
104
+ ...(fsSync.existsSync(userConfigPath) ? [{ file: userConfigPath, projectScoped: false }] : []),
105
+ ...(projectConfigPath ? [{ file: projectConfigPath, projectScoped: true }] : []),
106
+ ...(configPath ? [{ file: configPath, projectScoped: true }] : []),
107
+ ];
108
+ const seen = new Set();
109
+ let config = {
88
110
  ...defaultConfig,
89
- ...parsed,
90
- workspaceRoot: path.resolve(baseDir, parsed.workspaceRoot ?? defaultConfig.workspaceRoot),
91
- context: { ...defaultConfig.context, ...parsed.context },
92
- permissions: { ...defaultConfig.permissions, ...parsed.permissions },
93
- skills: { ...defaultConfig.skills, ...parsed.skills },
94
- mcpServers: parsed.mcpServers ?? defaultConfig.mcpServers,
95
- agents,
111
+ workspaceRoot: path.resolve(defaultConfig.workspaceRoot),
112
+ context: { ...defaultConfig.context },
113
+ permissions: { ...defaultConfig.permissions },
114
+ skills: { ...defaultConfig.skills },
115
+ mcpServers: { ...defaultConfig.mcpServers },
116
+ agents: [...defaultConfig.agents],
96
117
  };
118
+ for (const layer of layers) {
119
+ const resolvedConfigPath = path.resolve(layer.file);
120
+ if (seen.has(resolvedConfigPath))
121
+ continue;
122
+ seen.add(resolvedConfigPath);
123
+ const raw = await fs.readFile(resolvedConfigPath, "utf8");
124
+ const parsed = JSON.parse(raw);
125
+ const { workers: _workers, ...overrides } = parsed;
126
+ if (typeof overrides.systemPrompt === "string" && !overrides.systemPrompt.trim())
127
+ delete overrides.systemPrompt;
128
+ if (typeof overrides.modeInstructions === "string" && !overrides.modeInstructions.trim())
129
+ delete overrides.modeInstructions;
130
+ const configDir = path.dirname(resolvedConfigPath);
131
+ const baseDir = path.basename(configDir) === ".apollo" ? path.dirname(configDir) : configDir;
132
+ const agents = normalizeAgents(parsed.agents ?? parsed.workers) ?? config.agents;
133
+ const workspaceRoot = layer.projectScoped && parsed.workspaceRoot !== undefined
134
+ ? path.resolve(baseDir, parsed.workspaceRoot)
135
+ : config.workspaceRoot;
136
+ config = {
137
+ ...config,
138
+ ...overrides,
139
+ workspaceRoot,
140
+ context: { ...config.context, ...parsed.context },
141
+ permissions: { ...config.permissions, ...parsed.permissions },
142
+ skills: { ...config.skills, ...parsed.skills },
143
+ mcpServers: { ...config.mcpServers, ...parsed.mcpServers },
144
+ agents,
145
+ };
146
+ }
97
147
  validateAgentConfig(config);
98
148
  return config;
99
149
  }
package/dist/index.js CHANGED
@@ -3,7 +3,8 @@ import { stdin, stdout } from "node:process";
3
3
  import readline from "node:readline/promises";
4
4
  import { ASCII_LOGO, PRODUCT } from "./brand.js";
5
5
  import { parseArgs } from "./cli-args.js";
6
- import { ensureUserEnvFile, loadAgentConfig, loadEnvFile, loadLlmConfig, MISSING_AUTH_TOKEN_MESSAGE, } from "./config.js";
6
+ import { createPastedTextBlock, pastedTextPreview, renderInputView, resolvePastedText, } from "./cli-input.js";
7
+ import { ensureUserEnvFile, ensureUserConfigFile, loadAgentConfig, loadEnvFile, loadLlmConfig, MISSING_AUTH_TOKEN_MESSAGE, } from "./config.js";
7
8
  import { pickSpinnerVerb } from "./constants/spinner-verbs.js";
8
9
  import { QueryEngine } from "./runtime/query-engine.js";
9
10
  import { createCliApprovalProvider } from "./runtime/permissions.js";
@@ -13,7 +14,6 @@ import { ThoughtFoldManager } from "./thought-fold.js";
13
14
  import { workflowStatusBar } from "./coordinator/workflow.js";
14
15
  import { CliTraceRenderer, stripTerminalEscapes } from "./trace.js";
15
16
  let activeCliInput;
16
- let pinnedCliInput;
17
17
  let modalActive = false;
18
18
  let resolveModal;
19
19
  let modalDone;
@@ -23,6 +23,7 @@ let activeVerb = "Thinking";
23
23
  let assistantResponseStarted = false;
24
24
  /** Prevent double green/red outcome lines in one user turn */
25
25
  let outcomePrintedThisTurn = false;
26
+ let pastedTextSequence = 0;
26
27
  const slashCommands = [
27
28
  { command: "/help", usage: "/help", description: "Show slash commands" },
28
29
  { command: "/skill", usage: "/skill", description: "List installed skills" },
@@ -57,6 +58,7 @@ function filterSlashCommands(buffer) {
57
58
  async function main() {
58
59
  const args = parseArgs(process.argv.slice(2));
59
60
  const userEnv = ensureUserEnvFile();
61
+ const userConfig = ensureUserConfigFile();
60
62
  loadEnvFile(args.envPath);
61
63
  const task = args.task ?? (stdin.isTTY ? undefined : (await readStdin()).trim());
62
64
  if (!task && !stdin.isTTY) {
@@ -97,6 +99,7 @@ async function main() {
97
99
  const rendererSink = renderer.sink();
98
100
  const taskActivity = new TaskActivityLog();
99
101
  const inputHistory = [];
102
+ const pendingUserMessages = [];
100
103
  // traceSink closes over the runtime, but the runtime is created after this. Hold it in a
101
104
  // mutable ref that createRuntime populates, so goal_status events don't hit a TDZ (task mode
102
105
  // never reaches the interactive `runtime` binding). Also lets SIGINT close whichever is active.
@@ -239,6 +242,9 @@ async function main() {
239
242
  if (userEnv.created) {
240
243
  stdout.write(`\nCreated ${shortenHome(userEnv.path)}. Add ANTHROPIC_AUTH_TOKEN there, then restart Apollo.\n`);
241
244
  }
245
+ if (userConfig.created) {
246
+ stdout.write(`Created ${shortenHome(userConfig.path)} with default settings.\n`);
247
+ }
242
248
  const approvalProvider = createCliApprovalProvider(agentConfig, traceSink, () => yolo, {
243
249
  beforePrompt: () => {
244
250
  stopLiveStatus("none");
@@ -272,7 +278,7 @@ async function main() {
272
278
  await modalDone;
273
279
  continue;
274
280
  }
275
- const text = (await readCliLine({ color: traceOptions.color, history: inputHistory })).trim();
281
+ const text = (pendingUserMessages.shift() ?? await readCliLine({ color: traceOptions.color, history: inputHistory })).trim();
276
282
  if (!text)
277
283
  continue;
278
284
  if (text === "/exit" || text === "/quit") {
@@ -404,12 +410,12 @@ async function main() {
404
410
  outcomePrintedThisTurn = false;
405
411
  activeVerb = pickSpinnerVerb(goalBody);
406
412
  const firstPrompt = `Start Goal mode.\nGoal: ${goalBody}\nMake the first concrete step. Use tools as needed.\nEnd with GOAL_PROGRESS / GOAL_DONE / GOAL_BLOCKED / GOAL_FAILED.`;
407
- startPinnedCliInput(traceOptions.color);
413
+ const turnInput = startTurnInput(runtime, traceOptions.color, inputHistory, pendingUserMessages);
408
414
  try {
409
415
  await runGoalLoop(runtime, renderer, thoughtFold, traceOptions, firstPrompt);
410
416
  }
411
417
  finally {
412
- stopPinnedCliInput();
418
+ await turnInput.stop();
413
419
  }
414
420
  continue;
415
421
  }
@@ -529,12 +535,12 @@ async function main() {
529
535
  continue;
530
536
  }
531
537
  // ── user turn ──────────────────────────────────────────────
532
- // Keep an empty input chrome pinned below streaming output while this turn runs.
538
+ // Keep a live queue input below streaming output while this turn runs.
533
539
  runningTaskCount = 0;
534
540
  assistantResponseStarted = false;
535
541
  outcomePrintedThisTurn = false;
536
542
  activeVerb = pickSpinnerVerb(text);
537
- startPinnedCliInput(traceOptions.color);
543
+ const turnInput = startTurnInput(runtime, traceOptions.color, inputHistory, pendingUserMessages);
538
544
  try {
539
545
  if (!llmConfig.authToken)
540
546
  throw new Error(MISSING_AUTH_TOKEN_MESSAGE);
@@ -556,7 +562,7 @@ async function main() {
556
562
  writeAboveActiveInput(runtime.renderWorkflowBar(traceOptions.color));
557
563
  }
558
564
  void runtime.saveCurrentSession().catch(() => null);
559
- stopPinnedCliInput();
565
+ await turnInput.stop();
560
566
  }
561
567
  }
562
568
  const sessionId = runtime.getSessionId();
@@ -950,67 +956,42 @@ function writeAboveActiveInput(text) {
950
956
  input?.renderAfterExternalOutput();
951
957
  }
952
958
  }
953
- function startPinnedCliInput(color) {
954
- if (!stdin.isTTY || !stdout.isTTY || modalActive)
955
- return;
956
- stopPinnedCliInput();
957
- let renderedLines = 0;
958
- let renderedCursorLine = 0;
959
- const clear = () => {
960
- if (renderedLines === 0)
961
- return;
962
- if (renderedCursorLine > 0)
963
- stdout.write(`\x1b[${renderedCursorLine}A\r`);
964
- else
965
- stdout.write("\r");
966
- for (let index = 0; index < renderedLines; index += 1) {
967
- stdout.write("\x1b[2K");
968
- if (index < renderedLines - 1)
969
- stdout.write("\x1b[1B\r");
959
+ function startTurnInput(runtime, color, history, pendingMessages) {
960
+ if (!stdin.isTTY || !stdout.isTTY)
961
+ return { stop: async () => undefined };
962
+ let accepting = true;
963
+ const done = (async () => {
964
+ while (accepting) {
965
+ if (modalActive && modalDone) {
966
+ await modalDone;
967
+ continue;
968
+ }
969
+ const value = await readCliLine({
970
+ color,
971
+ history,
972
+ queueMode: true,
973
+ onEscape: () => {
974
+ const cancelled = runtime.cancelCurrentTurn();
975
+ if (cancelled)
976
+ accepting = false;
977
+ return cancelled;
978
+ },
979
+ });
980
+ const queued = value.trim();
981
+ if (queued)
982
+ pendingMessages.push(queued);
983
+ if (!accepting)
984
+ break;
970
985
  }
971
- if (renderedLines > 1)
972
- stdout.write(`\x1b[${renderedLines - 1}A\r`);
973
- renderedLines = 0;
974
- renderedCursorLine = 0;
975
- };
976
- const render = () => {
977
- if (modalActive || activeCliInput !== pinnedCliInput)
978
- return;
979
- clear();
980
- const statusLine = renderLiveStatusLine(color);
981
- const lines = [
982
- ...(statusLine ? [statusLine] : []),
983
- renderInputBorder("top", color),
984
- renderInputPrompt(color),
985
- renderInputBorder("bottom", color),
986
- ];
987
- stdout.write(lines.join("\n"));
988
- renderedLines = lines.length;
989
- renderedCursorLine = statusLine ? 2 : 1;
990
- const rowsBelowInput = lines.length - 1 - renderedCursorLine;
991
- stdout.write(`\x1b[${rowsBelowInput}A\r\x1b[3G`);
992
- };
993
- const pinned = {
994
- clearForExternalOutput: clear,
995
- renderAfterExternalOutput: render,
996
- cancelForModal: () => {
997
- clear();
998
- if (activeCliInput === pinned)
999
- activeCliInput = undefined;
986
+ })();
987
+ return {
988
+ stop: async () => {
989
+ accepting = false;
990
+ activeCliInput?.cancelForModal();
991
+ await done;
992
+ stdout.write("\x1b[?25h");
1000
993
  },
1001
994
  };
1002
- pinnedCliInput = pinned;
1003
- activeCliInput = pinned;
1004
- render();
1005
- }
1006
- function stopPinnedCliInput() {
1007
- const pinned = pinnedCliInput;
1008
- if (!pinned)
1009
- return;
1010
- pinned.clearForExternalOutput();
1011
- if (activeCliInput === pinned)
1012
- activeCliInput = undefined;
1013
- pinnedCliInput = undefined;
1014
995
  }
1015
996
  async function readCliLine(options) {
1016
997
  if (!stdin.isTTY || !stdout.isTTY) {
@@ -1032,6 +1013,7 @@ async function readCliLine(options) {
1032
1013
  let previousRawMode = false;
1033
1014
  let renderedCursorLine = 0;
1034
1015
  let pasteBuffer;
1016
+ const pastedTextBlocks = new Map();
1035
1017
  const pasteStart = "\x1b[200~";
1036
1018
  const pasteEnd = "\x1b[201~";
1037
1019
  const complete = (value) => {
@@ -1041,6 +1023,8 @@ async function readCliLine(options) {
1041
1023
  done = true;
1042
1024
  if (onData)
1043
1025
  stdin.off("data", onData);
1026
+ if (stdin.isTTY)
1027
+ stdout.write("\x1b[?25h");
1044
1028
  if (stdin.isTTY)
1045
1029
  stdout.write("\x1b[?2004l");
1046
1030
  if (stdin.isTTY)
@@ -1055,8 +1039,11 @@ async function readCliLine(options) {
1055
1039
  if (liveStatusLine)
1056
1040
  lines.push(liveStatusLine);
1057
1041
  lines.push(renderInputBorder("top", options.color));
1058
- const view = inputView(buffer, cursor, Math.max(20, (stdout.columns ?? 100) - 4));
1059
- lines.push(`${renderInputPrompt(options.color)}${view.text}`);
1042
+ const view = renderInputView(buffer, cursor, inputContentWidth(), pastedTextBlocks);
1043
+ const idleQueue = options.queueMode && !buffer;
1044
+ lines.push(`${renderInputPrompt(options.color)}${idleQueue ? renderTurnInputHint(options.color) : (view.lines[0] ?? "")}`);
1045
+ if (!idleQueue)
1046
+ lines.push(...view.lines.slice(1).map((line) => ` ${line}`));
1060
1047
  if (paletteOpen) {
1061
1048
  const filtered = filterSlashCommands(buffer);
1062
1049
  lines.push(...renderSlashPaletteLines(selected, options.color, filtered, buffer));
@@ -1088,21 +1075,31 @@ async function readCliLine(options) {
1088
1075
  const lines = linesForState();
1089
1076
  stdout.write(lines.join("\n"));
1090
1077
  renderedLines = lines.length;
1091
- renderedCursorLine = renderLiveStatusLine(options.color) ? 2 : 1;
1078
+ const view = renderInputView(buffer, cursor, inputContentWidth(), pastedTextBlocks);
1079
+ renderedCursorLine = (renderLiveStatusLine(options.color) ? 2 : 1) + view.cursorLine;
1092
1080
  const rowsBelowInput = lines.length - 1 - renderedCursorLine;
1093
- const view = inputView(buffer, cursor, Math.max(20, (stdout.columns ?? 100) - 4));
1094
1081
  stdout.write(`\x1b[${rowsBelowInput}A\r\x1b[${3 + view.cursorWidth}G`);
1082
+ stdout.write(options.queueMode && !buffer ? "\x1b[?25l" : "\x1b[?25h");
1095
1083
  };
1096
1084
  const finish = (value) => {
1097
1085
  clearRendered();
1098
- if (value) {
1099
- if (options.history.at(-1) !== value)
1100
- options.history.push(value);
1086
+ const resolvedValue = resolvePastedText(value, pastedTextBlocks);
1087
+ if (resolvedValue) {
1088
+ if (options.history.at(-1) !== resolvedValue)
1089
+ options.history.push(resolvedValue);
1101
1090
  if (options.history.length > 100)
1102
1091
  options.history.splice(0, options.history.length - 100);
1103
- stdout.write(`${renderInputBorder("top", options.color)}\n${renderInputPrompt(options.color)}${submittedInputLabel(value)}\n${renderInputBorder("bottom", options.color)}\n`);
1092
+ const preview = pastedTextPreview(value, pastedTextBlocks).replace(/\n/g, "").replace(/\t/g, " ");
1093
+ const submitted = preview === value ? submittedInputLabel(resolvedValue) : preview;
1094
+ if (options.queueMode) {
1095
+ const label = options.color ? "\x1b[90m• Queued\x1b[0m" : "• Queued";
1096
+ stdout.write(`${label} ${submitted}\n`);
1097
+ }
1098
+ else {
1099
+ stdout.write(`${renderInputBorder("top", options.color)}\n${renderInputPrompt(options.color)}${submitted}\n${renderInputBorder("bottom", options.color)}\n`);
1100
+ }
1104
1101
  }
1105
- complete(value);
1102
+ complete(resolvedValue);
1106
1103
  };
1107
1104
  const cancelForModal = () => {
1108
1105
  clearRendered();
@@ -1168,7 +1165,13 @@ async function readCliLine(options) {
1168
1165
  finish("/exit");
1169
1166
  return;
1170
1167
  }
1168
+ if (chunk === "\x1b" && options.onEscape?.()) {
1169
+ finish("");
1170
+ return;
1171
+ }
1171
1172
  if (chunk === "\r" || chunk === "\n") {
1173
+ if (options.queueMode && !buffer)
1174
+ return;
1172
1175
  finish(buffer);
1173
1176
  return;
1174
1177
  }
@@ -1240,11 +1243,11 @@ async function readCliLine(options) {
1240
1243
  if (historyIndex === options.history.length)
1241
1244
  historyDraft = buffer;
1242
1245
  historyIndex -= 1;
1243
- buffer = options.history[historyIndex] ?? "";
1246
+ setBuffer(options.history[historyIndex] ?? "");
1244
1247
  }
1245
1248
  else if (chunk === "\x1b[B" && historyIndex < options.history.length) {
1246
1249
  historyIndex += 1;
1247
- buffer = historyIndex === options.history.length ? historyDraft : options.history[historyIndex] ?? "";
1250
+ setBuffer(historyIndex === options.history.length ? historyDraft : options.history[historyIndex] ?? "");
1248
1251
  }
1249
1252
  cursor = Array.from(buffer).length;
1250
1253
  closePalette();
@@ -1271,13 +1274,31 @@ async function readCliLine(options) {
1271
1274
  render();
1272
1275
  };
1273
1276
  const insertText = (text) => {
1274
- const safeText = stripTerminalEscapes(text).replace(/\r\n?/g, "\n");
1277
+ const safeText = stripTerminalEscapes(text)
1278
+ .replace(/\r\n?/g, "\n")
1279
+ .replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, "");
1275
1280
  const characters = Array.from(buffer);
1276
- const inserted = Array.from(safeText);
1281
+ const block = createPastedTextBlock(safeText, pastedTextSequence + 1);
1282
+ if (block) {
1283
+ pastedTextSequence += 1;
1284
+ pastedTextBlocks.set(block.marker, block);
1285
+ }
1286
+ const inserted = block ? [block.marker] : Array.from(safeText);
1277
1287
  characters.splice(cursor, 0, ...inserted);
1278
1288
  buffer = characters.join("");
1279
1289
  cursor += inserted.length;
1280
1290
  };
1291
+ function setBuffer(value) {
1292
+ const block = createPastedTextBlock(value, pastedTextSequence + 1);
1293
+ if (block) {
1294
+ pastedTextSequence += 1;
1295
+ pastedTextBlocks.set(block.marker, block);
1296
+ buffer = block.marker;
1297
+ }
1298
+ else {
1299
+ buffer = value;
1300
+ }
1301
+ }
1281
1302
  const handleData = (chunk) => {
1282
1303
  let rest = chunk;
1283
1304
  while (rest) {
@@ -1340,10 +1361,6 @@ function endModalInput() {
1340
1361
  resolveModal?.();
1341
1362
  resolveModal = undefined;
1342
1363
  modalDone = undefined;
1343
- if (pinnedCliInput && !activeCliInput) {
1344
- activeCliInput = pinnedCliInput;
1345
- pinnedCliInput.renderAfterExternalOutput();
1346
- }
1347
1364
  }
1348
1365
  async function askPlainQuestion(prompt) {
1349
1366
  const wasRaw = stdin.isTTY ? stdin.isRaw : false;
@@ -1586,6 +1603,10 @@ function compactLine(text, max) {
1586
1603
  function renderInputPrompt(useColor) {
1587
1604
  return useColor ? "\x1b[38;2;96;165;250m›\x1b[0m " : "› ";
1588
1605
  }
1606
+ function renderTurnInputHint(useColor) {
1607
+ const hint = "Type to queue a message · Esc to interrupt";
1608
+ return useColor ? `\x1b[90m${hint}\x1b[0m` : hint;
1609
+ }
1589
1610
  function renderLiveStatusLine(useColor) {
1590
1611
  if (!liveStatus)
1591
1612
  return undefined;
@@ -1644,30 +1665,8 @@ function truncateDisplay(text, width) {
1644
1665
  }
1645
1666
  return `${result}…`;
1646
1667
  }
1647
- function inputView(value, cursor, width) {
1648
- const characters = Array.from(value);
1649
- const normalize = (text) => text.replace(/\n/g, " ↵ ").replace(/\t/g, " ");
1650
- const before = normalize(characters.slice(0, cursor).join(""));
1651
- const after = normalize(characters.slice(cursor).join(""));
1652
- if (displayLength(before) >= width) {
1653
- const visible = takeDisplayEnd(before, width - 1);
1654
- return { text: `…${visible}`, cursorWidth: 1 + displayLength(visible) };
1655
- }
1656
- const remaining = Math.max(0, width - displayLength(before));
1657
- const visibleAfter = remaining > 0 ? truncateDisplay(after, remaining) : "";
1658
- return { text: `${before}${visibleAfter}`, cursorWidth: displayLength(before) };
1659
- }
1660
- function takeDisplayEnd(text, width) {
1661
- const result = [];
1662
- let length = 0;
1663
- for (const char of Array.from(text).reverse()) {
1664
- const charWidth = displayLength(char);
1665
- if (length + charWidth > width)
1666
- break;
1667
- result.push(char);
1668
- length += charWidth;
1669
- }
1670
- return result.reverse().join("");
1668
+ function inputContentWidth() {
1669
+ return Math.max(20, Math.min(stdout.columns ?? 100, 100) - 4);
1671
1670
  }
1672
1671
  function submittedInputLabel(value) {
1673
1672
  if (!value.includes("\n"))
@@ -1,8 +1,7 @@
1
- /** Same spirit as CLAUDE.md / AGENTS.md discovery. */
2
- export declare const PROJECT_INSTRUCTION_FILES: readonly ["APOLLO.md", "CLAUDE.md", "AGENTS.md", ".apollo/instructions.md", ".claude/CLAUDE.md"];
1
+ export declare const PROJECT_INSTRUCTION_FILES: readonly ["APOLLO.md", "apollo.md", "AGENTS.md", ".apollo/instructions.md"];
3
2
  export type ProjectInstructions = {
4
3
  path: string;
5
4
  content: string;
6
5
  };
7
- export declare function loadProjectInstructions(workspaceRoot: string, maxChars?: number): Promise<ProjectInstructions | null>;
6
+ export declare function loadProjectInstructions(workspaceRoot: string, maxChars?: number, userHome?: string): Promise<ProjectInstructions | null>;
8
7
  //# sourceMappingURL=project-instructions.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"project-instructions.d.ts","sourceRoot":"","sources":["../src/project-instructions.ts"],"names":[],"mappings":"AAOA,sDAAsD;AACtD,eAAO,MAAM,yBAAyB,kGAM5B,CAAC;AAEX,MAAM,MAAM,mBAAmB,GAAG;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,wBAAsB,uBAAuB,CAC3C,aAAa,EAAE,MAAM,EACrB,QAAQ,SAAQ,GACf,OAAO,CAAC,mBAAmB,GAAG,IAAI,CAAC,CAgBrC"}
1
+ {"version":3,"file":"project-instructions.d.ts","sourceRoot":"","sources":["../src/project-instructions.ts"],"names":[],"mappings":"AAMA,eAAO,MAAM,yBAAyB,6EAK5B,CAAC;AAEX,MAAM,MAAM,mBAAmB,GAAG;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,wBAAsB,uBAAuB,CAC3C,aAAa,EAAE,MAAM,EACrB,QAAQ,SAAQ,EAChB,QAAQ,SAAe,GACtB,OAAO,CAAC,mBAAmB,GAAG,IAAI,CAAC,CAgCrC"}
@@ -1,18 +1,27 @@
1
- /**
2
- * Claude Code–style project instructions.
3
- * Loads the first matching file from the workspace root (in order).
4
- */
1
+ /** Apollo project instructions. Loads the first matching workspace file in order. */
5
2
  import fs from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
6
5
  import { safeRealResolve } from "./utils.js";
7
- /** Same spirit as CLAUDE.md / AGENTS.md discovery. */
8
6
  export const PROJECT_INSTRUCTION_FILES = [
9
7
  "APOLLO.md",
10
- "CLAUDE.md",
8
+ "apollo.md",
11
9
  "AGENTS.md",
12
10
  ".apollo/instructions.md",
13
- ".claude/CLAUDE.md",
14
11
  ];
15
- export async function loadProjectInstructions(workspaceRoot, maxChars = 24000) {
12
+ export async function loadProjectInstructions(workspaceRoot, maxChars = 24000, userHome = os.homedir()) {
13
+ const loaded = [];
14
+ for (const fileName of ["APOLLO.md", "apollo.md"]) {
15
+ try {
16
+ const content = (await fs.readFile(path.join(userHome, ".apollo", fileName), "utf8")).trim();
17
+ if (content)
18
+ loaded.push({ path: `~/.apollo/${fileName}`, content });
19
+ break;
20
+ }
21
+ catch {
22
+ // User-level instructions are optional.
23
+ }
24
+ }
16
25
  for (const rel of PROJECT_INSTRUCTION_FILES) {
17
26
  try {
18
27
  const full = await safeRealResolve(workspaceRoot, rel);
@@ -20,14 +29,20 @@ export async function loadProjectInstructions(workspaceRoot, maxChars = 24000) {
20
29
  const content = raw.trim();
21
30
  if (!content)
22
31
  continue;
23
- return {
24
- path: rel,
25
- content: content.length > maxChars ? `${content.slice(0, maxChars)}\n\n…(truncated)` : content,
26
- };
32
+ loaded.push({ path: rel, content });
33
+ break;
27
34
  }
28
35
  catch {
29
36
  // try next
30
37
  }
31
38
  }
32
- return null;
39
+ if (!loaded.length)
40
+ return null;
41
+ const combined = loaded
42
+ .map((instruction) => `## ${instruction.path}\n${instruction.content}`)
43
+ .join("\n\n");
44
+ return {
45
+ path: loaded.map((instruction) => instruction.path).join(" + "),
46
+ content: combined.length > maxChars ? `${combined.slice(0, maxChars)}\n\n…(truncated)` : combined,
47
+ };
33
48
  }
@@ -3,7 +3,7 @@ type PromptBuilderOptions = {
3
3
  agentConfig: AgentConfig;
4
4
  tools: AnthropicTool[];
5
5
  subagent?: SubagentConfig;
6
- /** Claude Code–style project instructions (APOLLO.md / CLAUDE.md / AGENTS.md) */
6
+ /** Apollo project instructions (APOLLO.md / AGENTS.md). */
7
7
  projectInstructions?: {
8
8
  path: string;
9
9
  content: string;
@@ -1 +1 @@
1
- {"version":3,"file":"prompt-builder.d.ts","sourceRoot":"","sources":["../../src/runtime/prompt-builder.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAI9E,KAAK,oBAAoB,GAAG;IAC1B,WAAW,EAAE,WAAW,CAAC;IACzB,KAAK,EAAE,aAAa,EAAE,CAAC;IACvB,QAAQ,CAAC,EAAE,cAAc,CAAC;IAC1B,iFAAiF;IACjF,mBAAmB,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;CAChE,CAAC;AAEF,qBAAa,aAAa;IACZ,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAAP,OAAO,EAAE,oBAAoB;IAE1D,KAAK,IAAI,MAAM;IAqBf,OAAO,CAAC,eAAe;IAmBvB,OAAO,CAAC,aAAa;IAUrB,OAAO,CAAC,0BAA0B;IASlC,OAAO,CAAC,yBAAyB;IAOjC,OAAO,CAAC,iBAAiB;IAWzB,OAAO,CAAC,uBAAuB;IAO/B,OAAO,CAAC,kBAAkB;IAa1B,OAAO,CAAC,aAAa;IAOrB,OAAO,CAAC,WAAW;CAQpB"}
1
+ {"version":3,"file":"prompt-builder.d.ts","sourceRoot":"","sources":["../../src/runtime/prompt-builder.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAI9E,KAAK,oBAAoB,GAAG;IAC1B,WAAW,EAAE,WAAW,CAAC;IACzB,KAAK,EAAE,aAAa,EAAE,CAAC;IACvB,QAAQ,CAAC,EAAE,cAAc,CAAC;IAC1B,2DAA2D;IAC3D,mBAAmB,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;CAChE,CAAC;AAEF,qBAAa,aAAa;IACZ,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAAP,OAAO,EAAE,oBAAoB;IAE1D,KAAK,IAAI,MAAM;IAqBf,OAAO,CAAC,eAAe;IAmBvB,OAAO,CAAC,aAAa;IAUrB,OAAO,CAAC,0BAA0B;IASlC,OAAO,CAAC,yBAAyB;IAOjC,OAAO,CAAC,iBAAiB;IAWzB,OAAO,CAAC,uBAAuB;IAO/B,OAAO,CAAC,kBAAkB;IAa1B,OAAO,CAAC,aAAa;IAOrB,OAAO,CAAC,WAAW;CAQpB"}
@@ -55,7 +55,7 @@ ${PRODUCT.tagline}`;
55
55
  if (!proj?.content)
56
56
  return "";
57
57
  return `# Project Instructions
58
- These instructions come from \`${proj.path}\` in the workspace. Follow them for this project.
58
+ These instructions were loaded from \`${proj.path}\`. Follow them for this project; project-level instructions take precedence over user-level instructions when they conflict.
59
59
 
60
60
  ${proj.content}`;
61
61
  }
@@ -1 +1 @@
1
- {"version":3,"file":"skills.d.ts","sourceRoot":"","sources":["../../src/skills/skills.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAK/C,qBAAa,YAAY;IAIX,OAAO,CAAC,QAAQ,CAAC,MAAM;IAHnC,OAAO,CAAC,YAAY,CAA2C;IAC/D,OAAO,CAAC,QAAQ,CAAC,YAAY,CAA6B;gBAE7B,MAAM,EAAE,WAAW;IAEhD,OAAO,CAAC,SAAS;IAMX,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAoBtF,QAAQ,IAAI,OAAO,CAAC,KAAK,CAAC;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAY/E,IAAI,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAaxC,YAAY,CAAC,UAAU,EAAE,MAAM,EAAE,aAAa,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IAuCvG,OAAO,CAAC,OAAO;YAKD,WAAW;CAuB1B"}
1
+ {"version":3,"file":"skills.d.ts","sourceRoot":"","sources":["../../src/skills/skills.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAK/C,qBAAa,YAAY;IAIX,OAAO,CAAC,QAAQ,CAAC,MAAM;IAHnC,OAAO,CAAC,YAAY,CAA2C;IAC/D,OAAO,CAAC,QAAQ,CAAC,YAAY,CAA6B;gBAE7B,MAAM,EAAE,WAAW;IAEhD,OAAO,CAAC,SAAS;IAMX,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAoBtF,QAAQ,IAAI,OAAO,CAAC,KAAK,CAAC;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAY/E,IAAI,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAaxC,YAAY,CAAC,UAAU,EAAE,MAAM,EAAE,aAAa,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IA2CvG,OAAO,CAAC,OAAO;YAKD,WAAW;CAuB1B"}
@@ -1,6 +1,6 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
- import { safeRealResolve, truncate } from "../utils.js";
3
+ import { truncate } from "../utils.js";
4
4
  export class SkillManager {
5
5
  config;
6
6
  catalogCache;
@@ -55,9 +55,13 @@ export class SkillManager {
55
55
  return truncate(body, 20000);
56
56
  }
57
57
  async installLocal(sourcePath, requestedName) {
58
- // Confine the source to the workspace (rejects ../ and symlink escapes) so the
59
- // install can't copy arbitrary files (e.g. /etc/passwd) into a skill dir that skill_read exposes.
60
- const source = await safeRealResolve(this.config.workspaceRoot, sourcePath);
58
+ // Local skill sources may live outside the active workspace (for example in a
59
+ // package cache or /tmp). Resolve the selected source canonically, while the
60
+ // installation target remains confined to the configured skills directory.
61
+ const requestedSource = path.isAbsolute(sourcePath)
62
+ ? sourcePath
63
+ : path.resolve(this.config.workspaceRoot, sourcePath);
64
+ const source = await fs.realpath(requestedSource);
61
65
  const stat = await fs.stat(source);
62
66
  const skillFile = stat.isDirectory() ? path.join(source, "SKILL.md") : source;
63
67
  if (path.basename(skillFile) !== "SKILL.md") {
@@ -331,12 +331,12 @@ export function createBuiltinRegistry(options) {
331
331
  });
332
332
  registry.register({
333
333
  name: "skill_install",
334
- description: "Install a local skill into the first configured skills directory. Source must be a SKILL.md file or a directory containing SKILL.md.",
334
+ description: "Install a local skill into the first configured skills directory. The source may be outside the workspace and must be a SKILL.md file or a directory containing SKILL.md.",
335
335
  risk: "high",
336
336
  input_schema: {
337
337
  type: "object",
338
338
  properties: {
339
- sourcePath: { type: "string", description: "Path to SKILL.md or a directory containing SKILL.md." },
339
+ sourcePath: { type: "string", description: "Local path, including outside the workspace, to SKILL.md or a directory containing SKILL.md." },
340
340
  name: { type: "string", description: "Optional installed skill name." },
341
341
  },
342
342
  required: ["sourcePath"],
package/docs/sdk.md CHANGED
@@ -89,7 +89,7 @@ const engine = await createQueryEngine({
89
89
 
90
90
  | 参数 | 类型 | 说明 |
91
91
  |---|---|---|
92
- | `configPath` | `string` | Agent 配置路径;默认发现 `.apollo/config.json`,兼容旧 `agent.config.json` |
92
+ | `configPath` | `string` | Agent 显式配置路径;在用户配置和项目配置之后覆盖,默认还会发现 `~/.apollo/config.json`、`.apollo/config.json`,并兼容旧 `agent.config.json` |
93
93
  | `envPath` | `string` | 可选环境变量文件;未提供模型配置时默认发现 `.apollo/.env`,然后 `.env` |
94
94
  | `agentConfig` | `AgentConfig` | 直接传入完整 Agent 配置;优先于 `configPath` |
95
95
  | `llmConfig` | `LlmConfig` | 直接传入模型配置;优先于环境变量 |
@@ -104,7 +104,7 @@ const engine = await createQueryEngine({
104
104
 
105
105
  ## 系统提示词与缓存
106
106
 
107
- `.apollo/config.json` 可以配置:
107
+ `~/.apollo/config.json` 和项目的 `.apollo/config.json` 都可以配置:
108
108
 
109
109
  ```json
110
110
  {
@@ -114,6 +114,10 @@ const engine = await createQueryEngine({
114
114
 
115
115
  Engine 创建时读取配置,首次请求时拼接 System Prompt 和工具定义,随后在该 Engine 生命周期内冻结。修改配置文件不会影响已经存在的 Engine,必须关闭并创建新 Engine。
116
116
 
117
+ 配置合并顺序为:内置默认值 → 用户配置 → 项目配置 → `configPath`。用户配置中的 `workspaceRoot` 会被忽略,避免全局配置把所有项目锁定到用户目录;工作区路径由项目配置或显式配置决定。
118
+
119
+ 系统提示词还会依次拼接用户级 `~/.apollo/APOLLO.md` 和项目级 `APOLLO.md`(兼容小写 `apollo.md`)。如果项目中没有 Apollo 指令文件,则继续读取通用的 `AGENTS.md` 或 `.apollo/instructions.md`。指令在 Engine 首次请求时冻结。
120
+
117
121
  运行中主要变化的是 `messages`,接近上下文上限时会执行语义压缩;稳定的 `system` 和 `tools` 有利于命中模型厂商的 Prompt Cache。
118
122
 
119
123
  ## 流式事件
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apolloyh/apollo-agent",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "type": "module",
5
5
  "description": "Apollo — your personal agent. Answer first, act when needed. Claude Code–style Task subagents.",
6
6
  "bin": {