@apolloyh/apollo-agent 0.1.3 → 0.1.5

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/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.5";
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.5",
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/index.js CHANGED
@@ -3,6 +3,7 @@ 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 { createPastedTextBlock, pastedTextPreview, renderInputView, resolvePastedText, } from "./cli-input.js";
6
7
  import { ensureUserEnvFile, 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";
@@ -23,6 +24,7 @@ let activeVerb = "Thinking";
23
24
  let assistantResponseStarted = false;
24
25
  /** Prevent double green/red outcome lines in one user turn */
25
26
  let outcomePrintedThisTurn = false;
27
+ let pastedTextSequence = 0;
26
28
  const slashCommands = [
27
29
  { command: "/help", usage: "/help", description: "Show slash commands" },
28
30
  { command: "/skill", usage: "/skill", description: "List installed skills" },
@@ -1032,6 +1034,7 @@ async function readCliLine(options) {
1032
1034
  let previousRawMode = false;
1033
1035
  let renderedCursorLine = 0;
1034
1036
  let pasteBuffer;
1037
+ const pastedTextBlocks = new Map();
1035
1038
  const pasteStart = "\x1b[200~";
1036
1039
  const pasteEnd = "\x1b[201~";
1037
1040
  const complete = (value) => {
@@ -1055,8 +1058,9 @@ async function readCliLine(options) {
1055
1058
  if (liveStatusLine)
1056
1059
  lines.push(liveStatusLine);
1057
1060
  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}`);
1061
+ const view = renderInputView(buffer, cursor, inputContentWidth(), pastedTextBlocks);
1062
+ lines.push(`${renderInputPrompt(options.color)}${view.lines[0] ?? ""}`);
1063
+ lines.push(...view.lines.slice(1).map((line) => ` ${line}`));
1060
1064
  if (paletteOpen) {
1061
1065
  const filtered = filterSlashCommands(buffer);
1062
1066
  lines.push(...renderSlashPaletteLines(selected, options.color, filtered, buffer));
@@ -1088,21 +1092,23 @@ async function readCliLine(options) {
1088
1092
  const lines = linesForState();
1089
1093
  stdout.write(lines.join("\n"));
1090
1094
  renderedLines = lines.length;
1091
- renderedCursorLine = renderLiveStatusLine(options.color) ? 2 : 1;
1095
+ const view = renderInputView(buffer, cursor, inputContentWidth(), pastedTextBlocks);
1096
+ renderedCursorLine = (renderLiveStatusLine(options.color) ? 2 : 1) + view.cursorLine;
1092
1097
  const rowsBelowInput = lines.length - 1 - renderedCursorLine;
1093
- const view = inputView(buffer, cursor, Math.max(20, (stdout.columns ?? 100) - 4));
1094
1098
  stdout.write(`\x1b[${rowsBelowInput}A\r\x1b[${3 + view.cursorWidth}G`);
1095
1099
  };
1096
1100
  const finish = (value) => {
1097
1101
  clearRendered();
1098
- if (value) {
1099
- if (options.history.at(-1) !== value)
1100
- options.history.push(value);
1102
+ const resolvedValue = resolvePastedText(value, pastedTextBlocks);
1103
+ if (resolvedValue) {
1104
+ if (options.history.at(-1) !== resolvedValue)
1105
+ options.history.push(resolvedValue);
1101
1106
  if (options.history.length > 100)
1102
1107
  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`);
1108
+ const preview = pastedTextPreview(value, pastedTextBlocks).replace(/\n/g, "").replace(/\t/g, " ");
1109
+ stdout.write(`${renderInputBorder("top", options.color)}\n${renderInputPrompt(options.color)}${preview === value ? submittedInputLabel(resolvedValue) : preview}\n${renderInputBorder("bottom", options.color)}\n`);
1104
1110
  }
1105
- complete(value);
1111
+ complete(resolvedValue);
1106
1112
  };
1107
1113
  const cancelForModal = () => {
1108
1114
  clearRendered();
@@ -1240,11 +1246,11 @@ async function readCliLine(options) {
1240
1246
  if (historyIndex === options.history.length)
1241
1247
  historyDraft = buffer;
1242
1248
  historyIndex -= 1;
1243
- buffer = options.history[historyIndex] ?? "";
1249
+ setBuffer(options.history[historyIndex] ?? "");
1244
1250
  }
1245
1251
  else if (chunk === "\x1b[B" && historyIndex < options.history.length) {
1246
1252
  historyIndex += 1;
1247
- buffer = historyIndex === options.history.length ? historyDraft : options.history[historyIndex] ?? "";
1253
+ setBuffer(historyIndex === options.history.length ? historyDraft : options.history[historyIndex] ?? "");
1248
1254
  }
1249
1255
  cursor = Array.from(buffer).length;
1250
1256
  closePalette();
@@ -1271,13 +1277,31 @@ async function readCliLine(options) {
1271
1277
  render();
1272
1278
  };
1273
1279
  const insertText = (text) => {
1274
- const safeText = stripTerminalEscapes(text).replace(/\r\n?/g, "\n");
1280
+ const safeText = stripTerminalEscapes(text)
1281
+ .replace(/\r\n?/g, "\n")
1282
+ .replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, "");
1275
1283
  const characters = Array.from(buffer);
1276
- const inserted = Array.from(safeText);
1284
+ const block = createPastedTextBlock(safeText, pastedTextSequence + 1);
1285
+ if (block) {
1286
+ pastedTextSequence += 1;
1287
+ pastedTextBlocks.set(block.marker, block);
1288
+ }
1289
+ const inserted = block ? [block.marker] : Array.from(safeText);
1277
1290
  characters.splice(cursor, 0, ...inserted);
1278
1291
  buffer = characters.join("");
1279
1292
  cursor += inserted.length;
1280
1293
  };
1294
+ function setBuffer(value) {
1295
+ const block = createPastedTextBlock(value, pastedTextSequence + 1);
1296
+ if (block) {
1297
+ pastedTextSequence += 1;
1298
+ pastedTextBlocks.set(block.marker, block);
1299
+ buffer = block.marker;
1300
+ }
1301
+ else {
1302
+ buffer = value;
1303
+ }
1304
+ }
1281
1305
  const handleData = (chunk) => {
1282
1306
  let rest = chunk;
1283
1307
  while (rest) {
@@ -1644,30 +1668,8 @@ function truncateDisplay(text, width) {
1644
1668
  }
1645
1669
  return `${result}…`;
1646
1670
  }
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("");
1671
+ function inputContentWidth() {
1672
+ return Math.max(20, Math.min(stdout.columns ?? 100, 100) - 4);
1671
1673
  }
1672
1674
  function submittedInputLabel(value) {
1673
1675
  if (!value.includes("\n"))
@@ -1 +1 @@
1
- {"version":3,"file":"skills.d.ts","sourceRoot":"","sources":["../../src/skills/skills.ts"],"names":[],"mappings":"AAGA,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;IAQX,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,7 +1,6 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
- import { fileURLToPath } from "node:url";
4
- import { safeRealResolve, truncate } from "../utils.js";
3
+ import { truncate } from "../utils.js";
5
4
  export class SkillManager {
6
5
  config;
7
6
  catalogCache;
@@ -10,11 +9,7 @@ export class SkillManager {
10
9
  this.config = config;
11
10
  }
12
11
  skillDirs() {
13
- const bundled = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../src/skills");
14
- return Array.from(new Set([
15
- ...this.config.skills.directories.map((dir) => path.resolve(this.config.workspaceRoot, dir)),
16
- bundled,
17
- ]));
12
+ return Array.from(new Set(this.config.skills.directories.map((dir) => path.resolve(this.config.workspaceRoot, dir))));
18
13
  }
19
14
  async search(query) {
20
15
  const results = [];
@@ -60,9 +55,13 @@ export class SkillManager {
60
55
  return truncate(body, 20000);
61
56
  }
62
57
  async installLocal(sourcePath, requestedName) {
63
- // Confine the source to the workspace (rejects ../ and symlink escapes) so the
64
- // install can't copy arbitrary files (e.g. /etc/passwd) into a skill dir that skill_read exposes.
65
- 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);
66
65
  const stat = await fs.stat(source);
67
66
  const skillFile = stat.isDirectory() ? path.join(source, "SKILL.md") : source;
68
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apolloyh/apollo-agent",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "type": "module",
5
5
  "description": "Apollo — your personal agent. Answer first, act when needed. Claude Code–style Task subagents.",
6
6
  "bin": {
@@ -20,7 +20,6 @@
20
20
  },
21
21
  "files": [
22
22
  "dist",
23
- "src/skills/tech-research-skill",
24
23
  "README.md",
25
24
  "docs/sdk.md",
26
25
  ".apollo/config.example.json",
@@ -1,200 +0,0 @@
1
- ---
2
- name: tech-research-skill
3
- description: "技术方向、行业领域、产品赛道、GitHub 仓库和开源项目的调查、研究、竞品分析、趋势分析和报告生成。Use when the user asks to investigate, research, evaluate, compare, analyze, score, summarize, or report on a technology direction, market/domain niche, AI infra, agent, RAG, LLM, devtool, data, security, web3, GitHub repository, GitHub URL, or open-source project. Handles GitHub repo search, repo health, issues/commits, implementation patterns, competitor landscape, market trend research, evidence sources, Markdown report output, and downloadable report links."
4
- ---
5
-
6
- # Tech Research Skill
7
-
8
- Use this skill for both:
9
-
10
- - **Direction research**: investigate a technology direction, industry/domain, product category, or open-source theme.
11
- - **Repository research**: investigate one or more GitHub repositories or open-source projects.
12
-
13
- Default to this skill for prompts like "调查数字人方向", "分析 RAG agent 赛道", "研究某 GitHub 项目", "竞品分析", "趋势分析", "开源项目尽调", or "生成研究报告".
14
-
15
- ## Tool Location
16
-
17
- The shell tools live in the same directory as this `SKILL.md`.
18
-
19
- Set the tool directory from the loaded skill path:
20
-
21
- ```sh
22
- TECH_RESEARCH_SKILL_DIR="$(dirname "$SKILL_PATH")"
23
- ```
24
-
25
- If the current shell is already running from this skill directory, use:
26
-
27
- ```sh
28
- TECH_RESEARCH_SKILL_DIR="."
29
- ```
30
-
31
- Do not use absolute paths. Do not run exploratory `find`, `ls`, or `echo $CLAUDE_SKILL_DIR` just to locate these tools. If path context is uncertain, call `skill_read` for `tech-research-skill` and derive the directory from the returned path.
32
-
33
- Tool role labels:
34
-
35
- - `repo_search`: `"$TECH_RESEARCH_SKILL_DIR/repo-fetch" --search "<query>" --per-page 10`
36
- - `repo_fetch`: `"$TECH_RESEARCH_SKILL_DIR/repo-fetch" owner/repo --include issues,commits`
37
- - `repo_analyze`: `"$TECH_RESEARCH_SKILL_DIR/repo-analyze"` reads `repo-fetch` JSON from stdin and emits deterministic local health signals. It does not call an LLM, does not use network, and does not require model credentials.
38
- - `repo_report_generate`: `"$TECH_RESEARCH_SKILL_DIR/report-generate" --template investment --format markdown --output <report>.md` renders `repo-analyze` JSON into Markdown or HTML. It does not call an LLM.
39
-
40
- These role labels are not separate executable filenames. The executable scripts are `repo-fetch`, `repo-analyze`, and `report-generate`.
41
-
42
- The outer agent is responsible for the actual judgment: repository health interpretation, implementation-pattern analysis, competitor analysis, market/trend synthesis, opportunity mapping, and final recommendation. Treat `repo-analyze` output as structured evidence, not as the final answer.
43
-
44
- ## GitHub API Token
45
-
46
- `repo-fetch` uses the GitHub REST API. Unauthenticated requests are rate-limited quickly, so prefer a local token.
47
-
48
- Supported environment variables:
49
-
50
- ```sh
51
- GITHUB_TOKEN=<github personal access token>
52
- GH_TOKEN=<github personal access token>
53
- ```
54
-
55
- `repo-fetch` also loads a local `.env` file from this skill directory. That `.env` file is ignored by git and may contain:
56
-
57
- ```sh
58
- GITHUB_TOKEN=<github personal access token>
59
- ```
60
-
61
- Never hardcode a GitHub token into `repo-fetch`, `SKILL.md`, reports, examples, command output, or committed files. If a token appears in chat/logs/source, tell the user to revoke and rotate it.
62
-
63
- ## Workflow A: Technology Direction Research
64
-
65
- Use this workflow when the user provides a field, technology direction, product category, or market/domain.
66
-
67
- ### Turn Budget Rules
68
-
69
- Finish with a report instead of continuing to search. For a normal direction investigation:
70
-
71
- - Run at most 4 GitHub search queries.
72
- - Fetch details for at most 4 representative repositories.
73
- - Fetch/search at most 5 web sources.
74
- - If GitHub or web search quality is poor twice, stop searching and write the report with limitations.
75
- - Do not run `date`, `ls docs`, or other housekeeping commands before writing the report.
76
- - Prefer a concise evidence-backed report over exhaustive searching.
77
- - Once there is enough evidence for a directional judgment, immediately write the Markdown report to the repository-root `docs/` directory. If the current working directory is `agent`, use `../docs/<filename>.md`; if the current working directory is this skill directory, use `../../../../docs/<filename>.md`. Do not write `docs/<filename>.md` from the `agent` directory, because that creates `agent/docs` and the frontend will not treat it as the canonical report location.
78
-
79
- 1. Frame the topic:
80
- - Normalize the topic name.
81
- - Identify adjacent keywords and English/Chinese search terms.
82
- - Decide whether the user needs build advice, investment analysis, competitor analysis, learning research, or a general trend report.
83
-
84
- 2. Search GitHub first:
85
-
86
- ```sh
87
- "$TECH_RESEARCH_SKILL_DIR/repo-fetch" --search "<topic keywords> stars:>100" --per-page 10
88
- ```
89
-
90
- Select 5-10 representative repositories:
91
-
92
- - Most-starred or widely used projects.
93
- - Recently active challengers.
94
- - Different implementation approaches.
95
- - Commercially relevant SDKs, frameworks, or infrastructure.
96
- - Niche projects that reveal emerging use cases.
97
-
98
- Do not rely only on stars. Prefer repos with recent commits, active issues/PRs, releases, real users, and clear positioning.
99
-
100
- 3. Analyze representative repositories:
101
-
102
- ```sh
103
- "$TECH_RESEARCH_SKILL_DIR/repo-fetch" owner/repo --include issues,commits \
104
- | "$TECH_RESEARCH_SKILL_DIR/repo-analyze"
105
- ```
106
-
107
- `repo-analyze` only computes local deterministic signals from the fetched GitHub data. The agent must read the JSON and perform the higher-level health analysis itself.
108
-
109
- 4. Search the web for trend evidence:
110
- - Use current authoritative sources: official docs, standards bodies, foundation pages, vendor announcements, market reports, funding/news coverage, developer surveys, ecosystem reports, benchmarks, regulation/compliance updates.
111
- - Collect source URLs and dates.
112
- - Distinguish observed evidence from inference.
113
-
114
- 5. Produce a report with this structure:
115
- - **Executive Judgment**: high/medium/low potential and why.
116
- - **GitHub Landscape**: repo table, implementation approaches, health, risks.
117
- - **Competitor Analysis**: direct open-source, commercial products, infrastructure platforms, substitutes.
118
- - **Trend and Market Signals**: drivers, tailwinds, headwinds, 6-18 month outlook.
119
- - **Opportunity Map**: underserved users, product wedges, technical differentiation, distribution/community angles.
120
- - **Recommendation**: build/watch/avoid, MVP scope, validation experiments, what to monitor.
121
- - **Evidence Sources**: final section with every repo, GitHub query, issue/commit dataset, and web page used.
122
-
123
- ## Workflow B: GitHub Repository Research
124
-
125
- Use this workflow when the user gives a GitHub URL, `owner/repo`, or asks for open-source project health.
126
-
127
- Convert GitHub URLs to `owner/repo`.
128
-
129
- Fetch and analyze:
130
-
131
- ```sh
132
- "$TECH_RESEARCH_SKILL_DIR/repo-fetch" owner/repo --include issues,commits \
133
- | "$TECH_RESEARCH_SKILL_DIR/repo-analyze" \
134
- | "$TECH_RESEARCH_SKILL_DIR/report-generate" --template weekly --format markdown
135
- ```
136
-
137
- For investment or opportunity analysis:
138
-
139
- ```sh
140
- "$TECH_RESEARCH_SKILL_DIR/repo-fetch" owner/repo --include issues,commits \
141
- | "$TECH_RESEARCH_SKILL_DIR/repo-analyze" \
142
- | "$TECH_RESEARCH_SKILL_DIR/report-generate" --template investment --format markdown
143
- ```
144
-
145
- The generated repository report is a starting template. For user-facing research, the agent should expand it with its own analysis, web evidence, competitor context, and a final evidence-source section.
146
-
147
- ## Report Output
148
-
149
- Always write final user-facing reports as Markdown under the repository root `docs/` directory, not under `agent/docs`.
150
-
151
- Canonical report locations by current working directory:
152
-
153
- ```sh
154
- # If cwd is the agent root:
155
- DOCS_DIR="../docs"
156
-
157
- # If cwd is this skill directory:
158
- DOCS_DIR="../../../../docs"
159
- ```
160
-
161
- Before writing, choose the path from the actual cwd. Never use `docs/<filename>.md` when cwd is `agent`.
162
-
163
- Use a lowercase slug plus timestamp when useful:
164
-
165
- ```text
166
- ../docs/digital-human-research-20260611-153000.md
167
- ../docs/repo-health-owner-repo-20260611-153000.md
168
- ```
169
-
170
- Do not put an absolute path in the report instructions.
171
-
172
- Use the dedicated file-writing tool when available. Do not use shell redirection or here-docs for writing the report if a `write_file` tool exists.
173
-
174
- Use a simple stable filename when no timestamp is available, such as `../docs/digital-human-research.md` from the agent root. It is better to overwrite a topic report than to spend a turn checking the clock or listing directories.
175
-
176
-
177
- Every final report must end with:
178
-
179
- ```md
180
- ## Evidence Sources
181
- ```
182
-
183
- Include:
184
-
185
- - GitHub repositories used as evidence.
186
- - GitHub search queries that shaped repo selection.
187
- - Issue/commit datasets used.
188
- - Web pages, articles, docs, market reports, announcements, and search results used for trend or competitor claims.
189
- - Source names, URLs, and publication/access dates when available.
190
-
191
- Do not put download links inside the Markdown report file, and do not manually add download links in the web frontend final answer. The frontend chat route appends report links after the assistant message once it detects the generated file. If a manual link is ever needed outside the frontend, the `file` query parameter must be only the basename in `docs`, not a path.
192
-
193
- ## Quality Bar
194
-
195
- - Use real repositories and real external sources; do not invent competitors or metrics.
196
- - Cite GitHub repos and web sources with links.
197
- - Prefer evidence-backed judgment over generic commentary.
198
- - Separate facts from inference.
199
- - Call out uncertainty, missing credentials, GitHub API limits, failed fetches, and weak evidence.
200
- - If the user asks in Chinese, answer in Chinese unless they request otherwise.
@@ -1,4 +0,0 @@
1
- interface:
2
- display_name: "Tech Research"
3
- short_description: "Research tech directions, repos, competitors, and trends"
4
- default_prompt: "Use $tech-research-skill to research a technology direction or GitHub repository and produce an evidence-backed report."