@harmonyos-arkts/opencode-plugin 0.0.6 → 0.0.8

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 (2) hide show
  1. package/dist/index.js +465 -219
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -14836,7 +14836,9 @@ function scheduleFlush() {
14836
14836
  }
14837
14837
  function log(message, data) {
14838
14838
  try {
14839
- const timestamp = (/* @__PURE__ */ new Date()).toISOString();
14839
+ const now = /* @__PURE__ */ new Date();
14840
+ const pad = (n) => String(n).padStart(2, "0");
14841
+ const timestamp = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}T${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`;
14840
14842
  const logEntry = `[${timestamp}] ${message} ${data ? JSON.stringify(data) : ""}
14841
14843
  `;
14842
14844
  buffer.push(logEntry);
@@ -14860,7 +14862,7 @@ var HM_DEVELOP = `
14860
14862
  ### Development Phase
14861
14863
  - Use the \`harmonyos-atomic-dev\` skill to implement features according to the design document for harmonyos atomic service
14862
14864
  - Use the \`harmonyos-atomic-dev\` skill to fix issues and modify function in current projects
14863
- - Use the createHmTemplate tool to create empty applicaiton\u3001module\u3001atomic
14865
+ - Use the createHmTemplate tool to create empty applicaiton\u3001module\u3001atomic.
14864
14866
  - Use the \`.harmonyos/\` file (*.md\u3001*.html) as references to generate
14865
14867
  - After completing development, you MUST proceed to the Build phase
14866
14868
  - **NO EXCEPTIONS**: Code must be built successfully before marking development as complete
@@ -14885,22 +14887,118 @@ var HM_BUILD = `
14885
14887
  5. If still failing after 3 attempts, report the remaining errors to the user and ask for guidance
14886
14888
  - Only after a successful build should the development cycle be considered complete
14887
14889
  `;
14888
- var HM_ASCF = `
14889
- ### ASCF Phase
14890
- - HM_ASCF is the default active environment. Treat all requests as ASCF-context tasks unless the user explicitly asks to switch domain.
14891
- - Skill routing is mandatory and must be explicit before execution. Never handle ASCF tasks without selecting one of the ASCF skills first.
14892
- - Use \`harmonyos-atomic-ascf-convert\` for mini-program to HarmonyOS Atomic Service conversion (migration, API mapping, directory and component adaptation, conversion implementation).
14893
- - Use \`harmonyos-ascf-knowledge\` for ASCF development guidance, technical Q&A, issue localization, and troubleshooting in existing ASCF projects.
14894
- - For mixed requests, execute in two stages: (1) conversion via \`harmonyos-atomic-ascf-convert\`, then (2) engineering verification and issue diagnosis via \`harmonyos-ascf-knowledge\`.
14895
- - If the request is "how to", "why it fails", "where the issue is", or "how to fix", always prioritize \`harmonyos-ascf-knowledge\`.
14896
- - If a selected skill returns insufficient context, immediately continue with the other ASCF skill as fallback, then merge results into a single ASCF-oriented answer.
14897
- `;
14898
14890
  var HM_ONE2MANY = `
14899
14891
  ### One-to-Many Adaptation Phase
14900
14892
  - Use the \`harmonyos-one2many-adapt\` skill to adapt the HarmonyOS application for multiple device types (phone, tablet, foldable, 2-in-1, wearable, TV)
14901
14893
  - After completing adaptation, you MUST proceed to the Build phase
14902
14894
  - **NO EXCEPTIONS**: Code must be built successfully before marking adaptation as complete
14903
14895
  `;
14896
+ var HM_PLAN = `
14897
+ ### Plan Phase
14898
+ You are a **planning specialist** for HarmonyOS projects. Your sole responsibility is to ANALYZE, PLAN, and DOCUMENT.
14899
+
14900
+ **CRITICAL RULES (you MUST follow these):**
14901
+ 1. You MUST follow ALL steps below in strict order. Do not skip any step.
14902
+ 2. You MUST NOT write any implementation code or create/edit source files (except the plan document).
14903
+ 3. You MUST write the final plan to \`.harmonyos/plan.md\`.
14904
+ 4. After outputting the plan, STOP and wait for user/approver feedback. Do NOT proceed to development yourself.
14905
+
14906
+ ---
14907
+
14908
+ **Step 1: Explore the Codebase**
14909
+
14910
+ Thoroughly explore the existing project to understand:
14911
+ - Directory structure and file organization (use Glob)
14912
+ - Architecture patterns \u2014 look for @ComponentV2/@ObservedV2, MVVM, state management singletons
14913
+ - Page routing setup (main_pages.json, route_map.json)
14914
+ - Existing models, stores, and data flow
14915
+ - Any existing features similar to what's being requested
14916
+ - module.json5 for permissions and ability configuration
14917
+ - Entry point (EntryAbility) and initial page loading
14918
+
14919
+ If \`.harmonyos/codebase.md\` exists, read it first for cached analysis.
14920
+
14921
+ **Step 2: Analyze Requirements**
14922
+
14923
+ Break down the user's request into:
14924
+ - Core features and sub-features
14925
+ - Dependencies between tasks (what must be done before what \u2014 use explicit "depends on Step N" markers)
14926
+ - Technical constraints (HarmonyOS NEXT API availability, Kit compatibility, permission requirements)
14927
+ - Impact analysis: which existing files need modification vs which are new files
14928
+
14929
+ Output a dependency-ordered task list.
14930
+
14931
+ **Step 3: Create the Development Plan**
14932
+
14933
+ Write a structured plan to \`.harmonyos/plan.md\` using this template:
14934
+
14935
+ \`\`\`markdown
14936
+ # Development Plan: [Feature Name]
14937
+
14938
+ ## Overview
14939
+ [1-2 sentence summary of what will be implemented]
14940
+
14941
+ ## Current Architecture Summary
14942
+ [Brief description of the relevant existing code that will be affected]
14943
+
14944
+ ## Implementation Steps
14945
+
14946
+ ### Step 1: [Clear Action Title]
14947
+ - **Objective**: [What this step accomplishes]
14948
+ - **Files to create**: [Full paths of new files, or "None"]
14949
+ - **Files to modify**: [Full paths of existing files to change, or "None"]
14950
+ - **Key changes**:
14951
+ - [Specific code changes: new classes, methods, UI components]
14952
+ - [APIs or Kits to use]
14953
+ - [State management approach]
14954
+ - [Configuration or permission updates]
14955
+ - **Acceptance criteria**: [How to verify this step is correctly completed]
14956
+
14957
+ ### Step 2: [Clear Action Title]
14958
+ [Same structure, with "depends on Step 1" if applicable]
14959
+ ...
14960
+
14961
+ ## Risk & Considerations
14962
+ - [Potential issues, edge cases, or breaking changes to existing functionality]
14963
+ \`\`\`
14964
+
14965
+ **Step 4: Present and Review**
14966
+
14967
+ Output the plan content to the user in your response. Then conclude with:
14968
+
14969
+ > Plan saved to \`.harmonyos/plan.md\`. Please review and confirm, or suggest adjustments before implementation begins.
14970
+
14971
+ **STOP HERE. Do NOT proceed to implementation. The main agent or user will handle the next phase.**
14972
+
14973
+ ---
14974
+
14975
+ **Granularity Guidelines:**
14976
+ - Simple tasks (1-2 files changed): 2-3 high-level steps
14977
+ - Medium tasks (3-5 files): 4-6 steps, one major file change per step
14978
+ - Complex tasks (6+ files or architectural changes): Fine-grained steps with explicit dependency chains
14979
+ - Each step MUST be independently verifiable
14980
+ - Group related changes that must be deployed together into the same step
14981
+
14982
+ **Plan Output Example (for reference):**
14983
+
14984
+ ### Step 1: Add UserStore Model
14985
+ - **Objective**: Create shared user authentication state management
14986
+ - **Files to create**: entry/src/main/ets/model/UserStore.ets
14987
+ - **Files to modify**: None
14988
+ - **Key changes**:
14989
+ - Create UserInfo interface and UserStore singleton with @ObservedV2/@Trace
14990
+ - Add loginWithHuaweiID() method using @kit.AccountKit authentication
14991
+ - Add logout() method
14992
+ - **Acceptance criteria**: UserStore exports getInstance(), isLoggedIn is reactive
14993
+
14994
+ ### Step 2: Create Splash Page with Countdown
14995
+ - **Objective**: Add splash screen that navigates based on login state
14996
+ - **Files to create**: entry/src/main/ets/pages/SplashPage.ets
14997
+ - **Files to modify**: entry/src/main/resources/base/profile/main_pages.json, entry/src/main/ets/entryability/EntryAbility.ets
14998
+ - **Key changes**:
14999
+ - ... (depends on Step 1)
15000
+ - **Acceptance criteria**: App starts on SplashPage, countdown works, routes correctly
15001
+ `;
14904
15002
  function buildPlanPrompt(type) {
14905
15003
  switch (type) {
14906
15004
  case "dev":
@@ -14909,10 +15007,10 @@ function buildPlanPrompt(type) {
14909
15007
  return HM_DESIGN;
14910
15008
  case "build":
14911
15009
  return HM_BUILD;
14912
- case "ascf":
14913
- return HM_ASCF;
14914
15010
  case "one2many":
14915
15011
  return HM_ONE2MANY;
15012
+ case "plan":
15013
+ return HM_PLAN;
14916
15014
  case "all":
14917
15015
  return buildAllAgentPrompt();
14918
15016
  default:
@@ -14920,40 +15018,16 @@ function buildPlanPrompt(type) {
14920
15018
  }
14921
15019
  }
14922
15020
  function buildAllAgentPrompt() {
14923
- return `All HarmonyOS development tasks can be broadly categorized into three main phases: Design, Development, Build, and ASCF conversion. Each phase has its own set of best practices and workflows to ensure efficient and successful project completion.
15021
+ return `All HarmonyOS development tasks can be broadly categorized into three main phases: Design, Development, Build. Each phase has its own set of best practices and workflows to ensure efficient and successful project completion.
14924
15022
  ${HM_DESIGN}
14925
15023
  ${HM_DEVELOP}
14926
15024
  ${HM_BUILD}
14927
- ${HM_ASCF}
14928
15025
 
14929
15026
  Follow these workflows:
14930
15027
  - **New project (greenfield)**: PRD Design \u2192 Development \u2192 Build.
14931
- - **Incremental development (complex)**: Explore the project first \u2192 Use Plan Agent to list a plan \u2192 Discuss plan details with the user \u2192 Development \u2192 Build.
14932
- - **Incremental development (simple)**: Development \u2192 Build directly.
14933
- - **WeChat mini-program conversion**: ASCF Phase \u2192 Development \u2192 Build.`;
14934
- }
14935
- function buildHmExploreSubAgentPrompt() {
14936
- return `
14937
- You are a HarmonyOS project file search specialist. You excel at thoroughly navigating and exploring codebases.
14938
-
14939
- Your strengths:
14940
- - Rapidly finding files using glob patterns
14941
- - Searching code and text with powerful regex patterns
14942
- - Reading and analyzing file contents
14943
-
14944
- Guidelines:
14945
- - Use Glob for broad file pattern matching
14946
- - Use Grep for searching file contents with regex
14947
- - Use Read when you know the specific file path you need to read
14948
- - Use Bash for file operations like copying, moving, or listing directory contents
14949
- - Adapt your search approach based on the thoroughness level specified by the caller
14950
- - Return file paths as absolute paths in your final response
14951
- - For clear communication, avoid using emojis
14952
- - Create '\`.harmonyos/codebase.md\`' to write your findings, and you can also read '\`.harmonyos/codebase.md\`' if this existed
14953
- - Do not create any files except '\`.harmonyos/codebase.md\`', or run bash commands that modify the user's system state in any way
14954
-
14955
- Complete the user's search request efficiently and report your findings clearly.
14956
- `;
15028
+ - **Complex tasks**: List a plan \u2192 Discuss plan details with the user if needed \u2192 Development \u2192 Build.
15029
+ - **Simple tasks**: Development \u2192 Build directly.
15030
+ `;
14957
15031
  }
14958
15032
  function buildHmAgentPrompt(type) {
14959
15033
  return `You are HarmonyOS Dev Assistant(HDACode), an expert coding agent specialized in HarmonyOS development.
@@ -15097,10 +15171,11 @@ function createHmAgent() {
15097
15171
  prompt: buildHmAgentPrompt("all"),
15098
15172
  temperature: 0.3,
15099
15173
  permission: {
15100
- "task": {
15101
- "explore": "deny"
15102
- },
15103
- "skillSearch": "deny"
15174
+ "*": "allow",
15175
+ doom_loop: "ask",
15176
+ external_directory: {
15177
+ "*": "ask"
15178
+ }
15104
15179
  },
15105
15180
  metadata: void 0
15106
15181
  };
@@ -15134,13 +15209,6 @@ function createDesignAgent() {
15134
15209
  }
15135
15210
 
15136
15211
  // src/agents/dev-agent.ts
15137
- function getHmExploreSubAgentDescription() {
15138
- return `
15139
- Fast agent specialized for exploring codebases.
15140
- Use this when you need to quickly find files by patterns (eg. "src/components/**/*.tsx"), search code for keywords (eg. "API endpoints"), or answer questions about the codebase (eg. "how do API endpoints work?").
15141
- When calling this agent, specify the desired thoroughness level: "quick" for basic searches, "medium" for moderate exploration, or "very thorough" for comprehensive analysis across multiple locations and naming conventions.
15142
- `;
15143
- }
15144
15212
  function getHmDevDescription() {
15145
15213
  return `
15146
15214
  HarmonyOS Development
@@ -15155,72 +15223,12 @@ function createHmDevelopmentAgent() {
15155
15223
  prompt: buildHmAgentPrompt("dev"),
15156
15224
  temperature: 0.3,
15157
15225
  permission: {
15158
- "task": {
15159
- "explore": "deny"
15160
- },
15161
15226
  "skillSearch": "allow"
15162
15227
  },
15163
15228
  metadata: void 0
15164
15229
  };
15165
15230
  return agent;
15166
15231
  }
15167
- function createHmExploreSubAgent() {
15168
- const agent = {
15169
- id: "harmonyos-explore",
15170
- name: "harmonyos-explore",
15171
- description: getHmExploreSubAgentDescription(),
15172
- mode: "subagent",
15173
- prompt: buildHmExploreSubAgentPrompt(),
15174
- temperature: 0.1,
15175
- permission: {
15176
- "*": "deny",
15177
- "grep": "allow",
15178
- "glob": "allow",
15179
- "list": "allow",
15180
- "bash": "allow",
15181
- "read": "allow",
15182
- "edit": {
15183
- "*.harmonyos/codebase.md": "allow"
15184
- },
15185
- "write": {
15186
- "*.harmonyos/codebase.md": "allow"
15187
- }
15188
- },
15189
- metadata: void 0
15190
- };
15191
- return agent;
15192
- }
15193
-
15194
- // src/agents/ascf-agent.ts
15195
- function getHmAscfDescription() {
15196
- return `
15197
- HarmonyOS ASCF assistant for mini-program conversion, ASCF development Q&A, and issue troubleshooting
15198
- `;
15199
- }
15200
- function createAscfAgent() {
15201
- const agent = {
15202
- id: "harmonyos-ascf",
15203
- name: "harmonyos-ascf",
15204
- description: getHmAscfDescription(),
15205
- mode: "primary",
15206
- prompt: buildHmAgentPrompt("ascf"),
15207
- temperature: 0.6,
15208
- permission: {
15209
- "skill": {
15210
- "*": "deny",
15211
- "harmonyos-atomic-ascf-convert": "allow",
15212
- "harmonyos-ascf-knowledge": "allow",
15213
- "huawei-payment-integration": "allow",
15214
- "harmonyos-atomic-service-filing": "allow",
15215
- "harmony-emulator-debugging": "allow",
15216
- "harmonyos-atomic-ascf-release": "allow"
15217
- },
15218
- "skillSearch": "deny"
15219
- },
15220
- metadata: void 0
15221
- };
15222
- return agent;
15223
- }
15224
15232
 
15225
15233
  // src/agents/one2many-agent.ts
15226
15234
  function getHmOne2ManyDescription() {
@@ -15251,9 +15259,7 @@ var AgentRegistry = class {
15251
15259
  this.register(createHmAgent());
15252
15260
  this.register(createDesignAgent());
15253
15261
  this.register(createHmDevelopmentAgent());
15254
- this.register(createAscfAgent());
15255
15262
  this.register(createOne2ManyAgent());
15256
- this.register(createHmExploreSubAgent());
15257
15263
  }
15258
15264
  register(agent) {
15259
15265
  this.agents.set(agent.id, agent);
@@ -15385,57 +15391,7 @@ var AgentManager = class {
15385
15391
  }
15386
15392
  };
15387
15393
 
15388
- // src/shared/ets-counter.ts
15389
- import { readdir, readFile as readFile2, stat } from "fs/promises";
15390
- import { join as join2, extname } from "path";
15391
- var SKIP_DIRS = /* @__PURE__ */ new Set([
15392
- "node_modules",
15393
- ".preview",
15394
- "build",
15395
- ".cxx",
15396
- ".gradle",
15397
- "oh_modules",
15398
- ".hvigor",
15399
- "entry/build"
15400
- ]);
15401
- async function collectEtsFiles(dir, results) {
15402
- let entries;
15403
- try {
15404
- entries = await readdir(dir, { withFileTypes: true });
15405
- } catch {
15406
- return;
15407
- }
15408
- for (const entry of entries) {
15409
- if (entry.isDirectory()) {
15410
- if (SKIP_DIRS.has(entry.name)) continue;
15411
- if (entry.name.startsWith(".") && entry.name !== ".ets") continue;
15412
- await collectEtsFiles(join2(dir, entry.name), results);
15413
- } else if (extname(entry.name) === ".ets") {
15414
- const filePath = join2(dir, entry.name);
15415
- try {
15416
- const content = await readFile2(filePath, "utf-8");
15417
- const lines = content.split("\n").length;
15418
- results.push({ path: filePath, lines });
15419
- } catch {
15420
- log("Failed to read .ets file", { path: filePath });
15421
- }
15422
- }
15423
- }
15424
- }
15425
- async function countEtsLines(projectDir) {
15426
- const files = [];
15427
- await collectEtsFiles(projectDir, files);
15428
- const totalLines = files.reduce((sum, f) => sum + f.lines, 0);
15429
- return {
15430
- fileCount: files.length,
15431
- totalLines,
15432
- files,
15433
- timestamp: Date.now()
15434
- };
15435
- }
15436
-
15437
15394
  // src/managers/config-handler.ts
15438
- var EXPLORE_ETS_THRESHOLD = 2e3;
15439
15395
  function mergePermission(target, source) {
15440
15396
  if (!target) return source;
15441
15397
  const result = { ...target };
@@ -15450,27 +15406,11 @@ function mergePermission(target, source) {
15450
15406
  }
15451
15407
  return result;
15452
15408
  }
15453
- function createConfigHandler(_pluginConfig, agent, projectDir) {
15409
+ function createConfigHandler(_pluginConfig, agent, _projectDir) {
15454
15410
  return async (config3) => {
15455
- let exploreEnabled = true;
15456
- try {
15457
- const stats = await countEtsLines(projectDir);
15458
- exploreEnabled = stats.totalLines >= EXPLORE_ETS_THRESHOLD;
15459
- log("ETS count for explore agent", {
15460
- totalLines: stats.totalLines,
15461
- fileCount: stats.fileCount,
15462
- enabled: exploreEnabled
15463
- });
15464
- } catch (err) {
15465
- log("Failed to count ETS lines, enabling explore by default", { error: String(err) });
15466
- }
15467
15411
  const pluginAgents = agent.record();
15468
15412
  if (config3.agent) {
15469
15413
  for (const [id, pluginAgentConfig] of Object.entries(pluginAgents)) {
15470
- if (id === "harmonyos-explore" && !exploreEnabled) {
15471
- log("Skipping harmonyos-explore agent (insufficient .ets files)");
15472
- continue;
15473
- }
15474
15414
  if (config3.agent[id]) {
15475
15415
  const merged = { ...config3.agent[id], ...pluginAgentConfig };
15476
15416
  if (config3.agent[id].permission && pluginAgentConfig.permission) {
@@ -15491,7 +15431,7 @@ function createConfigHandler(_pluginConfig, agent, projectDir) {
15491
15431
  template: "/harmony-development-env-check",
15492
15432
  description: "\u68C0\u67E5 HarmonyOS \u5F00\u53D1\u73AF\u5883\u914D\u7F6E\uFF08Node.js\u3001JDK\u3001ohpm\u3001hvigor\u3001DevEco SDK\uFF09"
15493
15433
  };
15494
- log("Config merged", config3.agent);
15434
+ log("Config merged");
15495
15435
  };
15496
15436
  }
15497
15437
 
@@ -28073,6 +28013,86 @@ async function downloadTemplate(mode, filePath) {
28073
28013
  }
28074
28014
  }
28075
28015
 
28016
+ // src/shared/ets-counter.ts
28017
+ import { readdir, readFile as readFile2, stat } from "fs/promises";
28018
+ import { join as join2, extname } from "path";
28019
+ var SKIP_DIRS = /* @__PURE__ */ new Set([
28020
+ "node_modules",
28021
+ ".preview",
28022
+ "build",
28023
+ ".cxx",
28024
+ ".gradle",
28025
+ "oh_modules",
28026
+ ".hvigor",
28027
+ "entry/build"
28028
+ ]);
28029
+ var HM_PROJECT_INDICATORS = ["build-profile.json5", "oh-package.json5"];
28030
+ var MAX_CONTENT_READ_FILES = 500;
28031
+ var ESTIMATED_LINES_PER_FILE = 100;
28032
+ async function isHarmonyOSProject(dir) {
28033
+ for (const indicator of HM_PROJECT_INDICATORS) {
28034
+ try {
28035
+ await stat(join2(dir, indicator));
28036
+ return true;
28037
+ } catch {
28038
+ }
28039
+ }
28040
+ return false;
28041
+ }
28042
+ async function collectEtsFilePaths(dir, results) {
28043
+ let entries;
28044
+ try {
28045
+ entries = await readdir(dir, { withFileTypes: true });
28046
+ } catch {
28047
+ return;
28048
+ }
28049
+ for (const entry of entries) {
28050
+ if (entry.isDirectory()) {
28051
+ if (SKIP_DIRS.has(entry.name)) continue;
28052
+ if (entry.name.startsWith(".") && entry.name !== ".ets") continue;
28053
+ await collectEtsFilePaths(join2(dir, entry.name), results);
28054
+ } else if (extname(entry.name) === ".ets") {
28055
+ results.push(join2(dir, entry.name));
28056
+ }
28057
+ }
28058
+ }
28059
+ async function countEtsLines(projectDir) {
28060
+ const startTime = Date.now();
28061
+ if (!await isHarmonyOSProject(projectDir)) {
28062
+ log("ets.count.skipped", { projectDir, elapsed: Date.now() - startTime });
28063
+ return { fileCount: 0, totalLines: 0, files: [], timestamp: Date.now() };
28064
+ }
28065
+ const filePaths = [];
28066
+ await collectEtsFilePaths(projectDir, filePaths);
28067
+ const fileCount = filePaths.length;
28068
+ if (fileCount > MAX_CONTENT_READ_FILES) {
28069
+ const elapsed2 = Date.now() - startTime;
28070
+ log("ets.count.estimated", { fileCount, estimatedLines: fileCount * ESTIMATED_LINES_PER_FILE, elapsed: elapsed2 });
28071
+ return {
28072
+ fileCount,
28073
+ totalLines: fileCount * ESTIMATED_LINES_PER_FILE,
28074
+ files: [],
28075
+ timestamp: Date.now()
28076
+ };
28077
+ }
28078
+ const files = [];
28079
+ for (const filePath of filePaths) {
28080
+ try {
28081
+ const content = await readFile2(filePath, "utf-8");
28082
+ files.push({ path: filePath, lines: content.split("\n").length });
28083
+ } catch {
28084
+ }
28085
+ }
28086
+ const elapsed = Date.now() - startTime;
28087
+ log("ets.count.precise", { fileCount, totalLines: files.reduce((sum, f) => sum + f.lines, 0), elapsed });
28088
+ return {
28089
+ fileCount,
28090
+ totalLines: files.reduce((sum, f) => sum + f.lines, 0),
28091
+ files,
28092
+ timestamp: Date.now()
28093
+ };
28094
+ }
28095
+
28076
28096
  // src/tools/create-template/ets-check.ts
28077
28097
  async function checkEtsExists(directory) {
28078
28098
  try {
@@ -28124,24 +28144,20 @@ function createHmTemplateTool(managers) {
28124
28144
  });
28125
28145
  }
28126
28146
 
28127
- // src/tools/skill-search/skill-search-tool.ts
28128
- import { homedir as homedir3 } from "node:os";
28129
- import { join as join4 } from "node:path";
28130
-
28131
28147
  // src/tools/skill-search/search-skill.ts
28132
- import { readFileSync } from "node:fs";
28133
- import { homedir as homedir2 } from "node:os";
28134
- import { join as join3 } from "node:path";
28148
+ import { readFileSync, existsSync } from "node:fs";
28149
+ import { join as join4 } from "node:path";
28135
28150
 
28136
28151
  // src/tools/skill-search/tokenizer.ts
28152
+ var CJK_RE = /[一-鿿㐀-䶿]/;
28153
+ var SEG_RE = /[一-鿿㐀-䶿]+|[\w@.]+/g;
28154
+ var CAMEL_RE = /[A-Z]?[a-z]+|[A-Z]+(?=[A-Z][a-z]|\b)|[0-9]+/g;
28137
28155
  var segmenter = new Intl.Segmenter("zh", { granularity: "word" });
28138
- var SEG_RE = /[一-鿿㐀-䶿]+|[^\s\p{P}一-鿿㐀-䶿]+/gu;
28139
- var CAMEL_RE = /[A-Z]?[a-z]+|[A-Z]+(?![a-z])/g;
28140
28156
  function tokenize(text) {
28141
28157
  const segments = text.match(SEG_RE) ?? [];
28142
28158
  const tokens = [];
28143
28159
  for (const seg of segments) {
28144
- if (/[一-鿿㐀-䶿]/.test(seg)) {
28160
+ if (CJK_RE.test(seg)) {
28145
28161
  for (const s of segmenter.segment(seg)) {
28146
28162
  if (s.isWordLike) tokens.push(s.segment.toLowerCase().trim());
28147
28163
  }
@@ -28157,7 +28173,7 @@ function tokenize(text) {
28157
28173
  }
28158
28174
  }
28159
28175
  }
28160
- return tokens.filter((w) => w.length > 0);
28176
+ return [...new Set(tokens.filter((w) => w.length > 0))];
28161
28177
  }
28162
28178
 
28163
28179
  // src/tools/skill-search/bm25.ts
@@ -28312,14 +28328,198 @@ var filter_default = [
28312
28328
  { id: "25", summary: "WaterFlow \u7011\u5E03\u6D41\u5E03\u5C40\u5F00\u53D1\u5B9E\u8DF5", path: "experience/experience_water_flow.md", category: "water_flow" }
28313
28329
  ];
28314
28330
 
28331
+ // src/tools/skill-search/build-sdk-index.ts
28332
+ import { readdir as readdir2, readFile as readFile3, writeFile } from "node:fs/promises";
28333
+ import { join as join3, relative, basename, dirname } from "node:path";
28334
+ var SCAN_DIRS = [
28335
+ "openharmony/js/api",
28336
+ "openharmony/ets/api",
28337
+ "openharmony/ets/component",
28338
+ "openharmony/ets/kits",
28339
+ "hms/ets/api",
28340
+ "hms/ets/kits"
28341
+ ];
28342
+ var TS_EXT_RE = /\.(?:d\.ts|d\.ets|ts|ets)$/;
28343
+ async function scanFiles(dir) {
28344
+ const results = [];
28345
+ async function walk(current) {
28346
+ const entries = await readdir2(current, { withFileTypes: true });
28347
+ for (const entry of entries) {
28348
+ const full = join3(current, entry.name);
28349
+ if (entry.isDirectory()) {
28350
+ await walk(full);
28351
+ } else if (TS_EXT_RE.test(entry.name)) {
28352
+ results.push(full);
28353
+ }
28354
+ }
28355
+ }
28356
+ await walk(dir);
28357
+ return results;
28358
+ }
28359
+ function extractSummary(content, filename, relPath) {
28360
+ const rawName = filename.replace(/\.(?:d\.ts|d\.ets|ts|ets)$/, "");
28361
+ let moduleName;
28362
+ if (rawName.startsWith("@")) {
28363
+ moduleName = rawName.replace(/^@/, "");
28364
+ } else {
28365
+ const dirParts = dirname(relPath).split("/").filter(Boolean);
28366
+ moduleName = [...dirParts, rawName].join(".");
28367
+ }
28368
+ const symbols = [];
28369
+ const stripped = content.replace(/\/\*\*[\s\S]*?\*\//g, "").replace(/\/\/.*$/gm, "");
28370
+ const patterns = [
28371
+ // Prefixed at file/module level
28372
+ /declare\s+namespace\s+(\w+)/g,
28373
+ /export\s+(?:default\s+)?interface\s+(\w+)/g,
28374
+ /export\s+(?:default\s+)?class\s+(\w+)/g,
28375
+ /export\s+(?:default\s+)?function\s+(\w+)/g,
28376
+ /export\s+(?:default\s+)?const\s+(\w+)/g,
28377
+ /export\s+(?:default\s+)?type\s+(\w+)/g,
28378
+ /export\s+(?:default\s+)?enum\s+(\w+)/g,
28379
+ /export\s+module\s+(\w+)/g,
28380
+ /declare\s+function\s+(\w+)/g,
28381
+ /declare\s+class\s+(\w+)/g,
28382
+ /declare\s+interface\s+(\w+)/g,
28383
+ /declare\s+const\s+(\w+)/g,
28384
+ /declare\s+type\s+(\w+)/g,
28385
+ /declare\s+enum\s+(\w+)/g,
28386
+ // Bare declarations inside declare namespace / module blocks
28387
+ /\bfunction\s+(\w+)\s*\(/g,
28388
+ /\binterface\s+(\w+)\s*(?:\{|extends)/g,
28389
+ /\benum\s+(\w+)\s*\{/g,
28390
+ /\bclass\s+(\w+)\s*(?:\{|extends|implements)/g,
28391
+ /\btype\s+(\w+)\s*=/g
28392
+ ];
28393
+ for (const pat of patterns) {
28394
+ let m;
28395
+ while ((m = pat.exec(stripped)) !== null) {
28396
+ if (!symbols.includes(m[1])) {
28397
+ symbols.push(m[1]);
28398
+ }
28399
+ }
28400
+ }
28401
+ const importNamedRe = /import\s+\{([^}]+)\}\s+from\s+/g;
28402
+ let mImport;
28403
+ while ((mImport = importNamedRe.exec(stripped)) !== null) {
28404
+ for (const name of mImport[1].split(",")) {
28405
+ const clean = name.trim().split(/\s+as\s+/)[0].trim();
28406
+ if (clean && !symbols.includes(clean)) {
28407
+ symbols.push(clean);
28408
+ }
28409
+ }
28410
+ }
28411
+ const importDefaultRe = /import\s+(\w+)\s+from\s+/g;
28412
+ let mDef;
28413
+ while ((mDef = importDefaultRe.exec(stripped)) !== null) {
28414
+ const name = mDef[1];
28415
+ if (!symbols.includes(name)) {
28416
+ symbols.push(name);
28417
+ }
28418
+ }
28419
+ const descMatch = content.match(
28420
+ /\*\s*(?:@description\s+)?(Provides[^.]*\.)/s
28421
+ );
28422
+ let providesDesc = "";
28423
+ if (descMatch) {
28424
+ providesDesc = descMatch[1].replace(/\*\s*/g, "").replace(/\n/g, " ").trim().slice(0, 200);
28425
+ }
28426
+ if (!providesDesc) {
28427
+ const gDesc = content.match(
28428
+ /\*\s*@description\s+([\s\S]*?)\s*\*\s*\//s
28429
+ );
28430
+ if (gDesc) {
28431
+ providesDesc = gDesc[1].replace(/\*\s*/g, "").replace(/\n/g, " ").trim().slice(0, 200);
28432
+ }
28433
+ }
28434
+ const kitMatch = content.match(/\*\s*@kit\s+(\w+)/);
28435
+ const kit = kitMatch ? kitMatch[1] : "";
28436
+ const syscapMatch = content.match(
28437
+ /\*\s*@syscap\s+SystemCapability\.([\w.]+)/
28438
+ );
28439
+ const syscap = syscapMatch ? syscapMatch[1] : "";
28440
+ const parts = [moduleName];
28441
+ if (kit && !moduleName.includes(kit)) parts.push(`Kit:${kit}`);
28442
+ if (syscap) parts.push(syscap);
28443
+ if (providesDesc) parts.push(providesDesc);
28444
+ if (symbols.length > 0) {
28445
+ parts.push(symbols.slice(0, 20).join(", "));
28446
+ }
28447
+ return {
28448
+ summary: parts.join(" | "),
28449
+ moduleName,
28450
+ symbols: symbols.slice(0, 50)
28451
+ };
28452
+ }
28453
+ async function buildSDKIndex(sdk_path) {
28454
+ let allFiles = [];
28455
+ for (const dir of SCAN_DIRS) {
28456
+ const fullDir = join3(sdk_path, "default", dir);
28457
+ log(`Scanning ${dir} ...`);
28458
+ const files = await scanFiles(fullDir);
28459
+ log(` Found ${files.length} files`);
28460
+ for (const f of files) {
28461
+ allFiles.push({
28462
+ absPath: f,
28463
+ relPath: relative(fullDir, f),
28464
+ sourceDir: dir
28465
+ });
28466
+ }
28467
+ }
28468
+ log(`Total files: ${allFiles.length}`);
28469
+ const entries = [];
28470
+ for (let i = 0; i < allFiles.length; i++) {
28471
+ const { absPath, relPath, sourceDir } = allFiles[i];
28472
+ const filename = basename(absPath);
28473
+ try {
28474
+ const content = await readFile3(absPath, "utf-8");
28475
+ const { summary, moduleName, symbols } = extractSummary(
28476
+ content,
28477
+ filename,
28478
+ relPath
28479
+ );
28480
+ entries.push({
28481
+ id: String(i + 1),
28482
+ summary,
28483
+ path: absPath,
28484
+ moduleName,
28485
+ symbols
28486
+ });
28487
+ } catch {
28488
+ }
28489
+ if ((i + 1) % 500 === 0) {
28490
+ log(`Processed ${i + 1}/${allFiles.length}`);
28491
+ }
28492
+ }
28493
+ const outputPath = join3(import.meta.dirname, "sdk-index.json");
28494
+ await writeFile(outputPath, JSON.stringify(entries, null, 2), "utf-8");
28495
+ log(`Done! Wrote ${entries.length} entries to sdk-index.json`);
28496
+ log(
28497
+ `Sample entries:`,
28498
+ entries.slice(0, 5).map((e) => e.moduleName)
28499
+ );
28500
+ }
28501
+
28315
28502
  // src/tools/skill-search/search-skill.ts
28316
- var DEFAULT_SKILL_PATH = join3(homedir2(), ".config", "opencode", "skills", "harmonyos-atomic-dev");
28317
28503
  var cachedEntries = null;
28318
28504
  var cachedIndexableEntries = null;
28319
28505
  var cachedFilters = null;
28320
28506
  var cachedRetriever = null;
28321
- function loadAndCache(resolvedPath) {
28322
- const raw = readFileSync(join3(resolvedPath, "index.json"), "utf-8");
28507
+ var cachedSdkEntries = null;
28508
+ var cachedSdkRetriever = null;
28509
+ async function loadAndCache(resolvedPath) {
28510
+ const sdk_path = findSdkDir();
28511
+ log("start load sdk cache", sdk_path);
28512
+ if (sdk_path !== void 0) {
28513
+ const sdkIndexPath = join4(import.meta.dirname, "sdk-index.json");
28514
+ if (!existsSync(sdkIndexPath)) {
28515
+ log("sdk path is not exist, need to build");
28516
+ await buildSDKIndex(sdk_path);
28517
+ }
28518
+ loadSdkIndex();
28519
+ log("success to load sdk cache");
28520
+ }
28521
+ log("start load skill cache");
28522
+ const raw = readFileSync(join4(resolvedPath, "index.json"), "utf-8");
28323
28523
  const entries = JSON.parse(raw);
28324
28524
  if (!Array.isArray(entries) || entries.length === 0) {
28325
28525
  return false;
@@ -28332,26 +28532,45 @@ function loadAndCache(resolvedPath) {
28332
28532
  const retriever = new HybridRetriever();
28333
28533
  retriever.addDocuments(documents, tokenizedDocs);
28334
28534
  cachedRetriever = retriever;
28535
+ log("success to load sdk cache");
28335
28536
  return true;
28336
28537
  }
28337
- function searchSkill(skill_path, query, topK = 5) {
28338
- const resolvedPath = skill_path?.trim() || DEFAULT_SKILL_PATH;
28538
+ function loadSdkIndex() {
28539
+ if (cachedSdkRetriever) return true;
28540
+ const sdkIndexPath = join4(import.meta.dirname, "sdk-index.json");
28541
+ if (!existsSync(sdkIndexPath)) return false;
28542
+ try {
28543
+ const raw = readFileSync(sdkIndexPath, "utf-8");
28544
+ const entries = JSON.parse(raw);
28545
+ if (!Array.isArray(entries) || entries.length === 0) return false;
28546
+ cachedSdkEntries = entries;
28547
+ const documents = entries.map((e) => e.summary);
28548
+ const tokenizedDocs = documents.map((d) => tokenize(d));
28549
+ const retriever = new BM25Retriever();
28550
+ retriever.addDocuments(documents, tokenizedDocs);
28551
+ cachedSdkRetriever = retriever;
28552
+ return true;
28553
+ } catch {
28554
+ return false;
28555
+ }
28556
+ }
28557
+ async function searchSkill(skill_path, query, topK = 3) {
28339
28558
  if (!cachedEntries) {
28340
28559
  try {
28341
- if (!loadAndCache(resolvedPath)) return [];
28560
+ if (!await loadAndCache(skill_path?.trim())) return [];
28342
28561
  } catch {
28343
28562
  return [];
28344
28563
  }
28345
28564
  }
28565
+ const prefix = skill_path.replace(/[\/\\]+$/, "");
28346
28566
  const preFiltered = [];
28347
28567
  for (const [category, entry] of cachedFilters) {
28348
28568
  const idx = query.toLowerCase().indexOf(category);
28349
28569
  if (idx !== -1) {
28350
28570
  preFiltered.push({
28351
28571
  ets_file_path: "",
28352
- experience_file_path: entry.path,
28353
- summary: entry.summary,
28354
- range: 0
28572
+ experience_file_path: join4(prefix, entry.path),
28573
+ sdk_file_path: ""
28355
28574
  });
28356
28575
  query = (query.slice(0, idx) + query.slice(idx + category.length)).trim();
28357
28576
  }
@@ -28359,23 +28578,42 @@ function searchSkill(skill_path, query, topK = 5) {
28359
28578
  if (!query) {
28360
28579
  return preFiltered;
28361
28580
  }
28581
+ const MIN_SCORE = 0.01;
28582
+ const sdkResults = loadSdkIndex() ? cachedSdkRetriever.search(query, topK).filter((r) => r.score >= MIN_SCORE).map((r) => ({
28583
+ ets_file_path: "",
28584
+ experience_file_path: "",
28585
+ sdk_file_path: cachedSdkEntries[r.index].path
28586
+ })) : [];
28362
28587
  const searchResults = cachedRetriever.search(query, topK);
28363
- const MIN_SCORE = 2e-3;
28364
28588
  const filtered = searchResults.filter((r) => r.score >= MIN_SCORE);
28365
- if (filtered.length === 0) return preFiltered;
28366
- return [...preFiltered, ...convert_search_result(filtered)];
28589
+ const skillResults = filtered.length > 0 ? aggregateByExperience(convert_search_result(filtered, prefix)) : [];
28590
+ return [...preFiltered, ...skillResults, ...sdkResults];
28591
+ }
28592
+ function aggregateByExperience(results) {
28593
+ const map3 = /* @__PURE__ */ new Map();
28594
+ for (const r of results) {
28595
+ const key = r.experience_file_path || r.ets_file_path;
28596
+ const existing = map3.get(key);
28597
+ if (existing) {
28598
+ if (r.ets_file_path) {
28599
+ existing.ets_file_path = existing.ets_file_path ? existing.ets_file_path + "\n" + r.ets_file_path : r.ets_file_path;
28600
+ }
28601
+ } else {
28602
+ map3.set(key, { ...r });
28603
+ }
28604
+ }
28605
+ return [...map3.values()];
28367
28606
  }
28368
- function convert_search_result(searchResults) {
28607
+ function convert_search_result(searchResults, prefix) {
28369
28608
  return searchResults.map((result, rank) => {
28370
28609
  const entry = cachedIndexableEntries[result.index];
28371
28610
  const experience = cachedEntries.find(
28372
28611
  (e) => e.type === "experience" && e.category === entry.category
28373
28612
  );
28374
28613
  return {
28375
- ets_file_path: entry.path,
28376
- experience_file_path: experience?.path ?? "",
28377
- summary: entry.summary,
28378
- range: rank + 1
28614
+ ets_file_path: join4(prefix, entry.path),
28615
+ experience_file_path: experience?.path ? join4(prefix, experience.path) : "",
28616
+ sdk_file_path: ""
28379
28617
  };
28380
28618
  });
28381
28619
  }
@@ -28383,19 +28621,27 @@ function convert_search_result(searchResults) {
28383
28621
  // src/tools/skill-search/skill-search-tool.ts
28384
28622
  function skillSearchTool(managers) {
28385
28623
  return tool({
28386
- description: "Search for relevant documents within the harmonyos-atomic-dev skill directory by keywords. Returns the top K most relevant document snippets from the skill directory, ranked by keyword match frequency. Use this tool instead of Glob/Grep when you need to find specific knowledge or documentation within harmonyos-atomic-dev skill. It scans all files under the given skill_path and returns the most relevant matches based on your query keywords. The results include ets_file_path (path to the ETS code examples), experience_file_path (path to the matching experience document best practices), summary (document summary), and range (relevance rank 1 = most relevant)\u3002",
28624
+ description: "Search for relevant documents within the harmonyos-atomic-dev skill directory by keywords. Returns the top K most relevant document snippets from the skill directory, ranked by keyword match frequency. Use this tool instead of Glob/Grep when you need to find specific knowledge or documentation within harmonyos-atomic-dev skill. The results include experience_file_path (path to the matching experience document best practices) and ets_file_path (path to the ETS code examples and SDK API file). IMPORTANT: Each scenario only supports ONE tool call. Do NOT call this tool multiple times.",
28387
28625
  args: {
28388
- skill_path: tool.schema.string("Absolute path to the skill directory to search within. Default path may not contains harmonyos-atomic-dev skill").default(join4(homedir3(), ".config", "opencode", "skills", "harmonyos-atomic-dev")),
28389
- query: tool.schema.string("A decomposed requirement or intent describing what you want to find, broken down into searchable keywords separated by spaces."),
28390
- topK: tool.schema.number("Maximum number of top-ranked documents to return. Actual results may be fewer depending on query relevance.").min(1).max(5).default(5)
28626
+ skill_path: tool.schema.string("Absolute path to the harmonyos-atomic-dev skill directory. IMPORTANT: this path should not contain any prefix or suffix like 'file://' or 'SKILL.md'."),
28627
+ query: tool.schema.string("A decomposed requirement or intent describing what you want to find, broken down into searchable keywords separated by spaces. QUERY TIPS: For best results, decompose your intent into short space-separated keywords instead of long sentences. Include: 1) Kit or component name (e.g. ScanKit, AdsKit, ShareKit), 2) specific API or method names (e.g. scanBarcode, loadAd, ShareController), 3) feature keywords (e.g. \u626B\u7801, \u5E7F\u544A\u52A0\u8F7D, \u5206\u4EAB). Example: 'ScanKit \u626B\u7801 scanBarcode startScanForResult' instead of '\u5E2E\u6211\u5B9E\u73B0\u4E00\u4E2A\u626B\u7801\u529F\u80FD'. "),
28628
+ topK: tool.schema.number("Maximum number of top-ranked documents to return. Actual results may be fewer depending on query relevance.").min(1).max(3).default(3)
28391
28629
  },
28392
28630
  execute: async (args, context) => {
28393
28631
  try {
28394
- const results = searchSkill(args.skill_path, args.query, args.topK);
28632
+ const results = await searchSkill(args.skill_path, args.query, args.topK);
28395
28633
  if (results.length === 0) {
28396
28634
  return "No relative items found in the skill directory.";
28397
28635
  }
28398
- return JSON.stringify(results, null, 2);
28636
+ const sdkInfo = results.map((r) => {
28637
+ const parts = [];
28638
+ if (r.experience_file_path) parts.push(`experience path ${r.experience_file_path}`);
28639
+ if (r.ets_file_path) parts.push(`sample code path ${r.ets_file_path}`);
28640
+ if (r.sdk_file_path) parts.push(`sdk info path ${r.sdk_file_path}`);
28641
+ return parts.join("\n");
28642
+ }).join("\n\n");
28643
+ return `You can read the following files as needed to obtain information.
28644
+ ${sdkInfo}`;
28399
28645
  } catch (e) {
28400
28646
  return `Error: ${e instanceof Error ? e.message : String(e)}`;
28401
28647
  }
@@ -28427,7 +28673,7 @@ function createTools(args) {
28427
28673
  }
28428
28674
 
28429
28675
  // src/hooks/tool-hooks.ts
28430
- import { readFile as readFile3 } from "fs/promises";
28676
+ import { readFile as readFile4 } from "fs/promises";
28431
28677
 
28432
28678
  // node_modules/diff/lib/index.mjs
28433
28679
  function Diff() {
@@ -29012,7 +29258,7 @@ function createToolHooks(sessionManager, projectDir) {
29012
29258
  if (input.tool === "write") {
29013
29259
  const filePath = output.args?.filePath;
29014
29260
  if (filePath) {
29015
- const oldContent = await readFile3(filePath, "utf-8").catch(() => "");
29261
+ const oldContent = await readFile4(filePath, "utf-8").catch(() => "");
29016
29262
  writeContentCache.set(input.callID, oldContent);
29017
29263
  }
29018
29264
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "@harmonyos-arkts/opencode-plugin",
4
- "version": "0.0.6",
4
+ "version": "0.0.8",
5
5
  "description": "HarmonyOS Full-Lifecycle Development Assistant. Specialized in the complete development lifecycle of HarmonyOS applications, including project creation, UI development, state management, network requests, data storage, permission requests, performance optimization, testing, and release.",
6
6
  "type": "module",
7
7
  "license": "MIT",
@@ -48,4 +48,4 @@
48
48
  "engines": {
49
49
  "node": ">=18.0.0"
50
50
  }
51
- }
51
+ }