@harmonyos-arkts/opencode-plugin 0.0.7 → 0.0.9

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/index.js CHANGED
@@ -14814,16 +14814,67 @@ await Promise.all([
14814
14814
 
14815
14815
  // src/shared/log.ts
14816
14816
  var logFile = path3.join(HMGlobal.Path.log, "plugin.log");
14817
+ var logDir = path3.dirname(logFile);
14818
+ var MAX_LOG_SIZE_BYTES = 5 * 1024 * 1024;
14819
+ var MAX_LOG_AGE_DAYS = 7;
14820
+ var MAX_ROTATED_FILES = 5;
14817
14821
  var buffer = [];
14818
14822
  var flushTimer = null;
14819
14823
  var FLUSH_INTERVAL_MS = 500;
14820
14824
  var BUFFER_SIZE_LIMIT = 50;
14825
+ function cleanupRotatedLogs() {
14826
+ try {
14827
+ const entries = fs2.readdirSync(logDir);
14828
+ const rotated = entries.filter((f) => f.startsWith("plugin.") && f.endsWith(".log") && f !== "plugin.log").map((f) => {
14829
+ const full = path3.join(logDir, f);
14830
+ try {
14831
+ return { file: full, mtimeMs: fs2.statSync(full).mtimeMs };
14832
+ } catch {
14833
+ return null;
14834
+ }
14835
+ }).filter((e) => e !== null);
14836
+ const now = Date.now();
14837
+ const maxAgeMs = MAX_LOG_AGE_DAYS * 24 * 60 * 60 * 1e3;
14838
+ for (const entry of rotated) {
14839
+ if (now - entry.mtimeMs > maxAgeMs) {
14840
+ try {
14841
+ fs2.unlinkSync(entry.file);
14842
+ } catch {
14843
+ }
14844
+ }
14845
+ }
14846
+ const surviving = rotated.filter((e) => now - e.mtimeMs <= maxAgeMs).sort((a, b) => a.mtimeMs - b.mtimeMs);
14847
+ if (surviving.length > MAX_ROTATED_FILES) {
14848
+ const doomed = surviving.slice(0, surviving.length - MAX_ROTATED_FILES);
14849
+ for (const entry of doomed) {
14850
+ try {
14851
+ fs2.unlinkSync(entry.file);
14852
+ } catch {
14853
+ }
14854
+ }
14855
+ }
14856
+ } catch {
14857
+ }
14858
+ }
14859
+ function rotateIfNeeded() {
14860
+ try {
14861
+ const stat3 = fs2.statSync(logFile);
14862
+ if (stat3.size >= MAX_LOG_SIZE_BYTES) {
14863
+ const now = /* @__PURE__ */ new Date();
14864
+ const pad = (n) => String(n).padStart(2, "0");
14865
+ const ts = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
14866
+ fs2.renameSync(logFile, path3.join(logDir, `plugin.${ts}.log`));
14867
+ }
14868
+ } catch {
14869
+ }
14870
+ }
14821
14871
  function flush() {
14822
14872
  if (buffer.length === 0) return;
14823
14873
  const data = buffer.join("");
14824
14874
  buffer = [];
14825
14875
  try {
14826
14876
  fs2.appendFileSync(logFile, data);
14877
+ rotateIfNeeded();
14827
14878
  } catch {
14828
14879
  }
14829
14880
  }
@@ -14836,7 +14887,9 @@ function scheduleFlush() {
14836
14887
  }
14837
14888
  function log(message, data) {
14838
14889
  try {
14839
- const timestamp = (/* @__PURE__ */ new Date()).toISOString();
14890
+ const now = /* @__PURE__ */ new Date();
14891
+ const pad = (n) => String(n).padStart(2, "0");
14892
+ const timestamp = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}T${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`;
14840
14893
  const logEntry = `[${timestamp}] ${message} ${data ? JSON.stringify(data) : ""}
14841
14894
  `;
14842
14895
  buffer.push(logEntry);
@@ -14848,6 +14901,7 @@ function log(message, data) {
14848
14901
  } catch {
14849
14902
  }
14850
14903
  }
14904
+ cleanupRotatedLogs();
14851
14905
 
14852
14906
  // src/agents/prompt.ts
14853
14907
  var HM_DESIGN = `
@@ -14860,7 +14914,7 @@ var HM_DEVELOP = `
14860
14914
  ### Development Phase
14861
14915
  - Use the \`harmonyos-atomic-dev\` skill to implement features according to the design document for harmonyos atomic service
14862
14916
  - 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. After creation, use parallel Read tool calls to read the template's README.md and key files of the current project simultaneously \u2014 do NOT use glob/grep to explore the template.
14917
+ - Use the createHmTemplate tool to create empty applicaiton\u3001module\u3001atomic.
14864
14918
  - Use the \`.harmonyos/\` file (*.md\u3001*.html) as references to generate
14865
14919
  - After completing development, you MUST proceed to the Build phase
14866
14920
  - **NO EXCEPTIONS**: Code must be built successfully before marking development as complete
@@ -15023,8 +15077,8 @@ ${HM_BUILD}
15023
15077
 
15024
15078
  Follow these workflows:
15025
15079
  - **New project (greenfield)**: PRD Design \u2192 Development \u2192 Build.
15026
- - **Incremental development (complex)**: Use Plan Agent to list a plan \u2192 Discuss plan details with the user \u2192 Development \u2192 Build.
15027
- - **Incremental development (simple)**: Development \u2192 Build directly.
15080
+ - **Complex tasks**: List a plan \u2192 Discuss plan details with the user if needed \u2192 Development \u2192 Build.
15081
+ - **Simple tasks**: Development \u2192 Build directly.
15028
15082
  `;
15029
15083
  }
15030
15084
  function buildHmAgentPrompt(type) {
@@ -15169,7 +15223,11 @@ function createHmAgent() {
15169
15223
  prompt: buildHmAgentPrompt("all"),
15170
15224
  temperature: 0.3,
15171
15225
  permission: {
15172
- "skillSearch": "deny"
15226
+ "*": "allow",
15227
+ doom_loop: "ask",
15228
+ external_directory: {
15229
+ "*": "ask"
15230
+ }
15173
15231
  },
15174
15232
  metadata: void 0
15175
15233
  };
@@ -15425,7 +15483,7 @@ function createConfigHandler(_pluginConfig, agent, _projectDir) {
15425
15483
  template: "/harmony-development-env-check",
15426
15484
  description: "\u68C0\u67E5 HarmonyOS \u5F00\u53D1\u73AF\u5883\u914D\u7F6E\uFF08Node.js\u3001JDK\u3001ohpm\u3001hvigor\u3001DevEco SDK\uFF09"
15427
15485
  };
15428
- log("Config merged", config3.agent);
15486
+ log("Config merged");
15429
15487
  };
15430
15488
  }
15431
15489
 
@@ -28138,23 +28196,20 @@ function createHmTemplateTool(managers) {
28138
28196
  });
28139
28197
  }
28140
28198
 
28141
- // src/tools/skill-search/skill-search-tool.ts
28142
- import { homedir as homedir2 } from "node:os";
28143
- import { join as join4 } from "node:path";
28144
-
28145
28199
  // src/tools/skill-search/search-skill.ts
28146
- import { readFileSync } from "node:fs";
28147
- import { join as join3 } from "node:path";
28200
+ import { readFileSync, existsSync } from "node:fs";
28201
+ import { join as join4 } from "node:path";
28148
28202
 
28149
28203
  // src/tools/skill-search/tokenizer.ts
28204
+ var CJK_RE = /[一-鿿㐀-䶿]/;
28205
+ var SEG_RE = /[一-鿿㐀-䶿]+|[\w@.]+/g;
28206
+ var CAMEL_RE = /[A-Z]?[a-z]+|[A-Z]+(?=[A-Z][a-z]|\b)|[0-9]+/g;
28150
28207
  var segmenter = new Intl.Segmenter("zh", { granularity: "word" });
28151
- var SEG_RE = /[一-鿿㐀-䶿]+|[^\s\p{P}一-鿿㐀-䶿]+/gu;
28152
- var CAMEL_RE = /[A-Z]?[a-z]+|[A-Z]+(?![a-z])/g;
28153
28208
  function tokenize(text) {
28154
28209
  const segments = text.match(SEG_RE) ?? [];
28155
28210
  const tokens = [];
28156
28211
  for (const seg of segments) {
28157
- if (/[一-鿿㐀-䶿]/.test(seg)) {
28212
+ if (CJK_RE.test(seg)) {
28158
28213
  for (const s of segmenter.segment(seg)) {
28159
28214
  if (s.isWordLike) tokens.push(s.segment.toLowerCase().trim());
28160
28215
  }
@@ -28170,7 +28225,7 @@ function tokenize(text) {
28170
28225
  }
28171
28226
  }
28172
28227
  }
28173
- return tokens.filter((w) => w.length > 0);
28228
+ return [...new Set(tokens.filter((w) => w.length > 0))];
28174
28229
  }
28175
28230
 
28176
28231
  // src/tools/skill-search/bm25.ts
@@ -28325,13 +28380,198 @@ var filter_default = [
28325
28380
  { id: "25", summary: "WaterFlow \u7011\u5E03\u6D41\u5E03\u5C40\u5F00\u53D1\u5B9E\u8DF5", path: "experience/experience_water_flow.md", category: "water_flow" }
28326
28381
  ];
28327
28382
 
28383
+ // src/tools/skill-search/build-sdk-index.ts
28384
+ import { readdir as readdir2, readFile as readFile3, writeFile } from "node:fs/promises";
28385
+ import { join as join3, relative, basename, dirname as dirname2 } from "node:path";
28386
+ var SCAN_DIRS = [
28387
+ "openharmony/js/api",
28388
+ "openharmony/ets/api",
28389
+ "openharmony/ets/component",
28390
+ "openharmony/ets/kits",
28391
+ "hms/ets/api",
28392
+ "hms/ets/kits"
28393
+ ];
28394
+ var TS_EXT_RE = /\.(?:d\.ts|d\.ets|ts|ets)$/;
28395
+ async function scanFiles(dir) {
28396
+ const results = [];
28397
+ async function walk(current) {
28398
+ const entries = await readdir2(current, { withFileTypes: true });
28399
+ for (const entry of entries) {
28400
+ const full = join3(current, entry.name);
28401
+ if (entry.isDirectory()) {
28402
+ await walk(full);
28403
+ } else if (TS_EXT_RE.test(entry.name)) {
28404
+ results.push(full);
28405
+ }
28406
+ }
28407
+ }
28408
+ await walk(dir);
28409
+ return results;
28410
+ }
28411
+ function extractSummary(content, filename, relPath) {
28412
+ const rawName = filename.replace(/\.(?:d\.ts|d\.ets|ts|ets)$/, "");
28413
+ let moduleName;
28414
+ if (rawName.startsWith("@")) {
28415
+ moduleName = rawName.replace(/^@/, "");
28416
+ } else {
28417
+ const dirParts = dirname2(relPath).split("/").filter(Boolean);
28418
+ moduleName = [...dirParts, rawName].join(".");
28419
+ }
28420
+ const symbols = [];
28421
+ const stripped = content.replace(/\/\*\*[\s\S]*?\*\//g, "").replace(/\/\/.*$/gm, "");
28422
+ const patterns = [
28423
+ // Prefixed at file/module level
28424
+ /declare\s+namespace\s+(\w+)/g,
28425
+ /export\s+(?:default\s+)?interface\s+(\w+)/g,
28426
+ /export\s+(?:default\s+)?class\s+(\w+)/g,
28427
+ /export\s+(?:default\s+)?function\s+(\w+)/g,
28428
+ /export\s+(?:default\s+)?const\s+(\w+)/g,
28429
+ /export\s+(?:default\s+)?type\s+(\w+)/g,
28430
+ /export\s+(?:default\s+)?enum\s+(\w+)/g,
28431
+ /export\s+module\s+(\w+)/g,
28432
+ /declare\s+function\s+(\w+)/g,
28433
+ /declare\s+class\s+(\w+)/g,
28434
+ /declare\s+interface\s+(\w+)/g,
28435
+ /declare\s+const\s+(\w+)/g,
28436
+ /declare\s+type\s+(\w+)/g,
28437
+ /declare\s+enum\s+(\w+)/g,
28438
+ // Bare declarations inside declare namespace / module blocks
28439
+ /\bfunction\s+(\w+)\s*\(/g,
28440
+ /\binterface\s+(\w+)\s*(?:\{|extends)/g,
28441
+ /\benum\s+(\w+)\s*\{/g,
28442
+ /\bclass\s+(\w+)\s*(?:\{|extends|implements)/g,
28443
+ /\btype\s+(\w+)\s*=/g
28444
+ ];
28445
+ for (const pat of patterns) {
28446
+ let m;
28447
+ while ((m = pat.exec(stripped)) !== null) {
28448
+ if (!symbols.includes(m[1])) {
28449
+ symbols.push(m[1]);
28450
+ }
28451
+ }
28452
+ }
28453
+ const importNamedRe = /import\s+\{([^}]+)\}\s+from\s+/g;
28454
+ let mImport;
28455
+ while ((mImport = importNamedRe.exec(stripped)) !== null) {
28456
+ for (const name of mImport[1].split(",")) {
28457
+ const clean = name.trim().split(/\s+as\s+/)[0].trim();
28458
+ if (clean && !symbols.includes(clean)) {
28459
+ symbols.push(clean);
28460
+ }
28461
+ }
28462
+ }
28463
+ const importDefaultRe = /import\s+(\w+)\s+from\s+/g;
28464
+ let mDef;
28465
+ while ((mDef = importDefaultRe.exec(stripped)) !== null) {
28466
+ const name = mDef[1];
28467
+ if (!symbols.includes(name)) {
28468
+ symbols.push(name);
28469
+ }
28470
+ }
28471
+ const descMatch = content.match(
28472
+ /\*\s*(?:@description\s+)?(Provides[^.]*\.)/s
28473
+ );
28474
+ let providesDesc = "";
28475
+ if (descMatch) {
28476
+ providesDesc = descMatch[1].replace(/\*\s*/g, "").replace(/\n/g, " ").trim().slice(0, 200);
28477
+ }
28478
+ if (!providesDesc) {
28479
+ const gDesc = content.match(
28480
+ /\*\s*@description\s+([\s\S]*?)\s*\*\s*\//s
28481
+ );
28482
+ if (gDesc) {
28483
+ providesDesc = gDesc[1].replace(/\*\s*/g, "").replace(/\n/g, " ").trim().slice(0, 200);
28484
+ }
28485
+ }
28486
+ const kitMatch = content.match(/\*\s*@kit\s+(\w+)/);
28487
+ const kit = kitMatch ? kitMatch[1] : "";
28488
+ const syscapMatch = content.match(
28489
+ /\*\s*@syscap\s+SystemCapability\.([\w.]+)/
28490
+ );
28491
+ const syscap = syscapMatch ? syscapMatch[1] : "";
28492
+ const parts = [moduleName];
28493
+ if (kit && !moduleName.includes(kit)) parts.push(`Kit:${kit}`);
28494
+ if (syscap) parts.push(syscap);
28495
+ if (providesDesc) parts.push(providesDesc);
28496
+ if (symbols.length > 0) {
28497
+ parts.push(symbols.slice(0, 20).join(", "));
28498
+ }
28499
+ return {
28500
+ summary: parts.join(" | "),
28501
+ moduleName,
28502
+ symbols: symbols.slice(0, 50)
28503
+ };
28504
+ }
28505
+ async function buildSDKIndex(sdk_path) {
28506
+ let allFiles = [];
28507
+ for (const dir of SCAN_DIRS) {
28508
+ const fullDir = join3(sdk_path, "default", dir);
28509
+ log(`Scanning ${dir} ...`);
28510
+ const files = await scanFiles(fullDir);
28511
+ log(` Found ${files.length} files`);
28512
+ for (const f of files) {
28513
+ allFiles.push({
28514
+ absPath: f,
28515
+ relPath: relative(fullDir, f),
28516
+ sourceDir: dir
28517
+ });
28518
+ }
28519
+ }
28520
+ log(`Total files: ${allFiles.length}`);
28521
+ const entries = [];
28522
+ for (let i = 0; i < allFiles.length; i++) {
28523
+ const { absPath, relPath, sourceDir } = allFiles[i];
28524
+ const filename = basename(absPath);
28525
+ try {
28526
+ const content = await readFile3(absPath, "utf-8");
28527
+ const { summary, moduleName, symbols } = extractSummary(
28528
+ content,
28529
+ filename,
28530
+ relPath
28531
+ );
28532
+ entries.push({
28533
+ id: String(i + 1),
28534
+ summary,
28535
+ path: absPath,
28536
+ moduleName,
28537
+ symbols
28538
+ });
28539
+ } catch {
28540
+ }
28541
+ if ((i + 1) % 500 === 0) {
28542
+ log(`Processed ${i + 1}/${allFiles.length}`);
28543
+ }
28544
+ }
28545
+ const outputPath = join3(import.meta.dirname, "sdk-index.json");
28546
+ await writeFile(outputPath, JSON.stringify(entries, null, 2), "utf-8");
28547
+ log(`Done! Wrote ${entries.length} entries to sdk-index.json`);
28548
+ log(
28549
+ `Sample entries:`,
28550
+ entries.slice(0, 5).map((e) => e.moduleName)
28551
+ );
28552
+ }
28553
+
28328
28554
  // src/tools/skill-search/search-skill.ts
28329
28555
  var cachedEntries = null;
28330
28556
  var cachedIndexableEntries = null;
28331
28557
  var cachedFilters = null;
28332
28558
  var cachedRetriever = null;
28333
- function loadAndCache(resolvedPath) {
28334
- const raw = readFileSync(join3(resolvedPath, "index.json"), "utf-8");
28559
+ var cachedSdkEntries = null;
28560
+ var cachedSdkRetriever = null;
28561
+ async function loadAndCache(resolvedPath) {
28562
+ const sdk_path = findSdkDir();
28563
+ log("start load sdk cache", sdk_path);
28564
+ if (sdk_path !== void 0) {
28565
+ const sdkIndexPath = join4(import.meta.dirname, "sdk-index.json");
28566
+ if (!existsSync(sdkIndexPath)) {
28567
+ log("sdk path is not exist, need to build");
28568
+ await buildSDKIndex(sdk_path);
28569
+ }
28570
+ loadSdkIndex();
28571
+ log("success to load sdk cache");
28572
+ }
28573
+ log("start load skill cache");
28574
+ const raw = readFileSync(join4(resolvedPath, "index.json"), "utf-8");
28335
28575
  const entries = JSON.parse(raw);
28336
28576
  if (!Array.isArray(entries) || entries.length === 0) {
28337
28577
  return false;
@@ -28344,23 +28584,45 @@ function loadAndCache(resolvedPath) {
28344
28584
  const retriever = new HybridRetriever();
28345
28585
  retriever.addDocuments(documents, tokenizedDocs);
28346
28586
  cachedRetriever = retriever;
28587
+ log("success to load sdk cache");
28347
28588
  return true;
28348
28589
  }
28349
- function searchSkill(skill_path, query, topK = 5) {
28590
+ function loadSdkIndex() {
28591
+ if (cachedSdkRetriever) return true;
28592
+ const sdkIndexPath = join4(import.meta.dirname, "sdk-index.json");
28593
+ if (!existsSync(sdkIndexPath)) return false;
28594
+ try {
28595
+ const raw = readFileSync(sdkIndexPath, "utf-8");
28596
+ const entries = JSON.parse(raw);
28597
+ if (!Array.isArray(entries) || entries.length === 0) return false;
28598
+ cachedSdkEntries = entries;
28599
+ const documents = entries.map((e) => e.summary);
28600
+ const tokenizedDocs = documents.map((d) => tokenize(d));
28601
+ const retriever = new BM25Retriever();
28602
+ retriever.addDocuments(documents, tokenizedDocs);
28603
+ cachedSdkRetriever = retriever;
28604
+ return true;
28605
+ } catch {
28606
+ return false;
28607
+ }
28608
+ }
28609
+ async function searchSkill(skill_path, query, topK = 3) {
28350
28610
  if (!cachedEntries) {
28351
28611
  try {
28352
- if (!loadAndCache(skill_path?.trim())) return [];
28612
+ if (!await loadAndCache(skill_path?.trim())) return [];
28353
28613
  } catch {
28354
28614
  return [];
28355
28615
  }
28356
28616
  }
28617
+ const prefix = skill_path.replace(/[\/\\]+$/, "");
28357
28618
  const preFiltered = [];
28358
28619
  for (const [category, entry] of cachedFilters) {
28359
28620
  const idx = query.toLowerCase().indexOf(category);
28360
28621
  if (idx !== -1) {
28361
28622
  preFiltered.push({
28362
28623
  ets_file_path: "",
28363
- experience_file_path: entry.path
28624
+ experience_file_path: join4(prefix, entry.path),
28625
+ sdk_file_path: ""
28364
28626
  });
28365
28627
  query = (query.slice(0, idx) + query.slice(idx + category.length)).trim();
28366
28628
  }
@@ -28368,21 +28630,42 @@ function searchSkill(skill_path, query, topK = 5) {
28368
28630
  if (!query) {
28369
28631
  return preFiltered;
28370
28632
  }
28633
+ const MIN_SCORE = 0.01;
28634
+ const sdkResults = loadSdkIndex() ? cachedSdkRetriever.search(query, topK).filter((r) => r.score >= MIN_SCORE).map((r) => ({
28635
+ ets_file_path: "",
28636
+ experience_file_path: "",
28637
+ sdk_file_path: cachedSdkEntries[r.index].path
28638
+ })) : [];
28371
28639
  const searchResults = cachedRetriever.search(query, topK);
28372
- const MIN_SCORE = 2e-3;
28373
28640
  const filtered = searchResults.filter((r) => r.score >= MIN_SCORE);
28374
- if (filtered.length === 0) return preFiltered;
28375
- return [...preFiltered, ...convert_search_result(filtered)];
28641
+ const skillResults = filtered.length > 0 ? aggregateByExperience(convert_search_result(filtered, prefix)) : [];
28642
+ return [...preFiltered, ...skillResults, ...sdkResults];
28643
+ }
28644
+ function aggregateByExperience(results) {
28645
+ const map3 = /* @__PURE__ */ new Map();
28646
+ for (const r of results) {
28647
+ const key = r.experience_file_path || r.ets_file_path;
28648
+ const existing = map3.get(key);
28649
+ if (existing) {
28650
+ if (r.ets_file_path) {
28651
+ existing.ets_file_path = existing.ets_file_path ? existing.ets_file_path + "\n" + r.ets_file_path : r.ets_file_path;
28652
+ }
28653
+ } else {
28654
+ map3.set(key, { ...r });
28655
+ }
28656
+ }
28657
+ return [...map3.values()];
28376
28658
  }
28377
- function convert_search_result(searchResults) {
28659
+ function convert_search_result(searchResults, prefix) {
28378
28660
  return searchResults.map((result, rank) => {
28379
28661
  const entry = cachedIndexableEntries[result.index];
28380
28662
  const experience = cachedEntries.find(
28381
28663
  (e) => e.type === "experience" && e.category === entry.category
28382
28664
  );
28383
28665
  return {
28384
- ets_file_path: entry.path,
28385
- experience_file_path: experience?.path ?? ""
28666
+ ets_file_path: join4(prefix, entry.path),
28667
+ experience_file_path: experience?.path ? join4(prefix, experience.path) : "",
28668
+ sdk_file_path: ""
28386
28669
  };
28387
28670
  });
28388
28671
  }
@@ -28390,25 +28673,27 @@ function convert_search_result(searchResults) {
28390
28673
  // src/tools/skill-search/skill-search-tool.ts
28391
28674
  function skillSearchTool(managers) {
28392
28675
  return tool({
28393
- 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). 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'.",
28676
+ 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.",
28394
28677
  args: {
28395
- 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(homedir2(), ".config", "opencode", "skills", "harmonyos-atomic-dev")),
28396
- query: tool.schema.string("A decomposed requirement or intent describing what you want to find, broken down into searchable keywords separated by spaces."),
28397
- 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)
28678
+ 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'."),
28679
+ 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'. "),
28680
+ 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)
28398
28681
  },
28399
28682
  execute: async (args, context) => {
28400
28683
  try {
28401
- const results = searchSkill(args.skill_path, args.query, args.topK);
28684
+ const results = await searchSkill(args.skill_path, args.query, args.topK);
28402
28685
  if (results.length === 0) {
28403
28686
  return "No relative items found in the skill directory.";
28404
28687
  }
28405
- return results.map((r) => {
28406
- if (r.ets_file_path) {
28407
- return `${r.experience_file_path}
28408
- \u793A\u4F8B\u6587\u4EF6\uFF1A${r.ets_file_path}`;
28409
- }
28410
- return r.experience_file_path;
28688
+ const sdkInfo = results.map((r) => {
28689
+ const parts = [];
28690
+ if (r.experience_file_path) parts.push(`experience path ${r.experience_file_path}`);
28691
+ if (r.ets_file_path) parts.push(`sample code path ${r.ets_file_path}`);
28692
+ if (r.sdk_file_path) parts.push(`sdk info path ${r.sdk_file_path}`);
28693
+ return parts.join("\n");
28411
28694
  }).join("\n\n");
28695
+ return `You can read the following files as needed to obtain information.
28696
+ ${sdkInfo}`;
28412
28697
  } catch (e) {
28413
28698
  return `Error: ${e instanceof Error ? e.message : String(e)}`;
28414
28699
  }
@@ -28440,7 +28725,7 @@ function createTools(args) {
28440
28725
  }
28441
28726
 
28442
28727
  // src/hooks/tool-hooks.ts
28443
- import { readFile as readFile3 } from "fs/promises";
28728
+ import { readFile as readFile4 } from "fs/promises";
28444
28729
 
28445
28730
  // node_modules/diff/lib/index.mjs
28446
28731
  function Diff() {
@@ -29025,7 +29310,7 @@ function createToolHooks(sessionManager, projectDir) {
29025
29310
  if (input.tool === "write") {
29026
29311
  const filePath = output.args?.filePath;
29027
29312
  if (filePath) {
29028
- const oldContent = await readFile3(filePath, "utf-8").catch(() => "");
29313
+ const oldContent = await readFile4(filePath, "utf-8").catch(() => "");
29029
29314
  writeContentCache.set(input.callID, oldContent);
29030
29315
  }
29031
29316
  }
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.7",
4
+ "version": "0.0.9",
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",
@@ -1,52 +0,0 @@
1
- # HarmonyOS Application Template
2
-
3
- HarmonyOS 标准应用工程模板,基于 Stage 模型,适用于开发完整的 HarmonyOS 应用程序。
4
-
5
- ## 目录结构
6
-
7
- ```
8
- application/
9
- ├── AppScope/ # 应用全局配置
10
- │ ├── app.json5 # 应用包名、版本号、图标等全局信息
11
- │ └── resources/ # 应用级资源(图标、字符串)
12
- ├── entry/ # 主模块(Entry Module)
13
- │ ├── src/
14
- │ │ ├── main/
15
- │ │ │ ├── ets/
16
- │ │ │ │ ├── entryability/ # UIAbility 入口
17
- │ │ │ │ ├── entrybackupability/# 备份恢复 ExtensionAbility
18
- │ │ │ │ └── pages/ # 页面(Index.ets)
19
- │ │ │ ├── resources/ # 模块资源(字符串、颜色、图片、配置)
20
- │ │ │ └── module.json5 # 模块配置(Ability、页面路由)
21
- │ │ ├── ohosTest/ # 仪器化测试
22
- │ │ └── test/ # 本地单元测试
23
- │ ├── build-profile.json5 # 模块级构建配置
24
- │ ├── oh-package.json5 # 模块依赖声明
25
- │ └── hvigorfile.ts # Hvigor 构建脚本
26
- ├── hvigor/ # Hvigor 构建引擎配置
27
- ├── build-profile.json5 # 工程级构建配置(签名、SDK 版本、模块列表)
28
- ├── oh-package.json5 # 工程级依赖声明
29
- ├── hvigorfile.ts # 工程级 Hvigor 脚本
30
- └── code-linter.json5 # 代码检查配置
31
- ```
32
-
33
- ## 关键配置
34
-
35
- | 配置项 | 值 |
36
- |--------|-----|
37
- | bundleName | `com.example.app` |
38
- | module type | `entry` |
39
- | targetSdkVersion | `6.0.1(21)` |
40
- | runtimeOS | `HarmonyOS` |
41
- | installationFree | `false` |
42
-
43
- ## 使用场景
44
-
45
- - 创建一个新的 HarmonyOS 标准应用工程
46
- - 包含完整的 UIAbility 生命周期管理
47
- - 内置备份恢复能力(EntryBackupAbility)
48
- - 适合需要独立安装和分发的应用
49
-
50
- ## 使用方式
51
-
52
- 此模板由 `createHmTemplate` 工具自动下载和解压,作为 HarmonyOS 应用开发的初始工程骨架。
@@ -1,62 +0,0 @@
1
- # HarmonyOS Atomic Service Template
2
-
3
- HarmonyOS 元服务(Atomic Service)工程模板,基于 Stage 模型,适用于开发免安装的轻量化服务。
4
-
5
- ## 目录结构
6
-
7
- ```
8
- atomic/
9
- ├── AppScope/ # 应用全局配置
10
- │ ├── app.json5 # 应用包名、版本号(bundleType: "atomicService")
11
- │ └── resources/ # 应用级资源(图标、字符串)
12
- ├── entry/ # 主模块(Entry Module)
13
- │ ├── src/
14
- │ │ ├── main/
15
- │ │ │ ├── ets/
16
- │ │ │ │ ├── entryability/ # UIAbility 入口
17
- │ │ │ │ └── pages/ # 页面(Index.ets)
18
- │ │ │ ├── resources/ # 模块资源(字符串、颜色、图片、配置)
19
- │ │ │ └── module.json5 # 模块配置(Ability、页面路由)
20
- │ │ ├── ohosTest/ # 仪器化测试
21
- │ │ ├── test/ # 本地单元测试
22
- │ │ └── mock/ # Mock 配置
23
- │ ├── build-profile.json5 # 模块级构建配置
24
- │ ├── oh-package.json5 # 模块依赖声明
25
- │ └── hvigorfile.ts # Hvigor 构建脚本
26
- ├── hvigor/ # Hvigor 构建引擎配置
27
- ├── build-profile.json5 # 工程级构建配置(签名、SDK 版本、模块列表)
28
- ├── oh-package.json5 # 工程级依赖声明
29
- ├── hvigorfile.ts # 工程级 Hvigor 脚本
30
- └── code-linter.json5 # 代码检查配置
31
- ```
32
-
33
- ## 关键配置
34
-
35
- | 配置项 | 值 |
36
- |--------|-----|
37
- | bundleName | `com.atomicservice.example` |
38
- | bundleType | `atomicService` |
39
- | module type | `entry` |
40
- | targetSdkVersion | `6.0.1(21)` |
41
- | runtimeOS | `HarmonyOS` |
42
- | installationFree | `true` |
43
-
44
- ## 与 Application 模板的区别
45
-
46
- | 特性 | Application | Atomic Service |
47
- |------|-------------|----------------|
48
- | bundleType | 默认(app) | `atomicService` |
49
- | installationFree | `false` | `true` |
50
- | 备份能力 | 包含 EntryBackupAbility | 不包含 |
51
- | 图标资源 | layered_image | app_icon |
52
- | 免安装 | 不支持 | 支持 |
53
-
54
- ## 使用场景
55
-
56
- - 开发免安装的元服务(Atomic Service)
57
- - 适用于服务卡片、快捷服务、轻量级功能入口
58
- - 通过 HarmonyOS 服务分发平台进行分发
59
-
60
- ## 使用方式
61
-
62
- 此模板由 `createHmTemplate` 工具自动下载和解压,作为 HarmonyOS 元服务开发的初始工程骨架。
@@ -1,42 +0,0 @@
1
- # HarmonyOS Module Template
2
-
3
- HarmonyOS HAR(HarmonyOS Archive)库模块模板,用于开发可复用的共享库模块。
4
-
5
- ## 目录结构
6
-
7
- ```
8
- module/
9
- ├── src/
10
- │ ├── main/
11
- │ │ ├── ets/
12
- │ │ │ └── components/ # 导出的组件(MainPage.ets)
13
- │ │ ├── resources/ # 模块资源(字符串、浮点数)
14
- │ │ └── module.json5 # 模块配置(type: "har")
15
- │ ├── ohosTest/ # 仪器化测试
16
- │ └── test/ # 本地单元测试
17
- ├── Index.ets # 库模块导出入口
18
- ├── build-profile.json5 # 模块级构建配置
19
- ├── oh-package.json5 # 模块依赖声明
20
- ├── hvigorfile.ts # Hvigor 构建脚本
21
- ├── consumer-rules.txt # 混淆消费者规则
22
- └── obfuscation-rules.txt # 混淆规则
23
- ```
24
-
25
- ## 关键配置
26
-
27
- | 配置项 | 值 |
28
- |--------|-----|
29
- | module name | `library` |
30
- | module type | `har` |
31
- | deviceTypes | `["phone"]` |
32
- | apiType | `stageMode` |
33
-
34
- ## 使用场景
35
-
36
- - 创建一个可复用的 HAR 共享库
37
- - 封装通用组件、工具类或业务逻辑供多个模块引用
38
- - 通过 `oh-package.json5` 管理依赖,支持混淆配置
39
-
40
- ## 使用方式
41
-
42
- 此模板由 `createHmTemplate` 工具自动下载和解压,用于在现有 HarmonyOS 工程中添加新的 HAR 库模块。将模板内容复制到工程目录下并在 `build-profile.json5` 的 `modules` 中注册即可。