@aiwg/cli 2026.9.12 → 2026.9.16

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 (36) hide show
  1. package/agentic/code/providers/capability-matrix.yaml +42 -0
  2. package/agentic/code/providers/model-capabilities.v1.json +119 -34
  3. package/agentic/code/providers/model-catalog.v1.json +259 -56
  4. package/dist/src/cli/handlers/sessions.js +348 -42
  5. package/dist/src/cli/handlers/use.js +20 -1
  6. package/dist/src/cli/scope-resolver.js +13 -0
  7. package/dist/src/cli/services/deployment-verification.js +33 -2
  8. package/dist/src/config/aiwg-config.js +39 -0
  9. package/dist/src/config/user-registry.js +9 -0
  10. package/dist/src/extensions/manifest.js +1 -0
  11. package/dist/src/models/model-capabilities.v1.json +119 -34
  12. package/dist/src/models/model-catalog.v1.json +259 -56
  13. package/dist/src/models/model-discovery.js +7 -0
  14. package/dist/src/models/provider-policy.js +1 -1
  15. package/dist/src/plugin/skill-command-translator.js +1 -0
  16. package/dist/src/providers/capability-matrix.yaml +42 -0
  17. package/dist/src/providers/grokbot-paths.js +103 -0
  18. package/dist/src/providers/provider-definitions.js +85 -1
  19. package/dist/src/providers/provider-inventory.js +1 -0
  20. package/dist/src/sessions/adapters/claude.js +127 -3
  21. package/dist/src/sessions/adapters/grokbot.js +49 -0
  22. package/dist/src/sessions/adapters/hermes.js +17 -9
  23. package/dist/src/sessions/adapters/opencode.js +12 -1
  24. package/dist/src/sessions/contracts.js +1 -1
  25. package/dist/src/sessions/export-plan.js +243 -0
  26. package/dist/src/sessions/fortemi-export-mapping.js +300 -0
  27. package/dist/src/sessions/fortemi-shard-blobs.js +78 -0
  28. package/dist/src/sessions/fortemi-shard-recovery.js +151 -0
  29. package/dist/src/sessions/index.js +6 -0
  30. package/dist/src/sessions/output-lineage.js +43 -0
  31. package/dist/src/sessions/workspace-discovery.js +1 -1
  32. package/dist/src/skills/deployer.js +17 -0
  33. package/dist/src/smiths/skillsmith/collision-detector.js +1 -0
  34. package/package.json +1 -1
  35. package/tools/agents/deploy-agents.mjs +3 -2
  36. package/tools/agents/providers/grokbot.mjs +285 -0
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Recovers an `AiwgFortemiIndexExport`-shaped record graph from a real
3
+ * `full-v1`/`2.0.0` Knowledge Shard archive built by
4
+ * `aiwgFortemiIndexToKnowledgeShardWithReport` (#2564).
5
+ *
6
+ * `@fortemi/core`'s own `aiwgFortemiIndexFromKnowledgeShard` only recovers
7
+ * archives built by the plain `aiwgFortemiIndexToKnowledgeShard` (`core-v1`):
8
+ * that function embeds the entire original record verbatim as opaque note
9
+ * metadata (`metadata.aiwg_fortemi_index`), which is what its reader looks
10
+ * for. The strict `full-v1` converter deliberately does not do this -- the
11
+ * point of `full-v1` is a genuinely native note/tag/link/provenance
12
+ * representation any Fortemi consumer can read, not an AIWG-only escape
13
+ * hatch. Verified empirically: `aiwgFortemiIndexFromKnowledgeShard` throws
14
+ * "has no AIWG index metadata" against a `full-v1` archive, and even AIWG's
15
+ * own artifact-index shard export test only round-trips through it using
16
+ * `profile: "core-v1"`.
17
+ *
18
+ * So recovering a `full-v1` archive means reading the native shard files
19
+ * directly and inverting this module's own mapping:
20
+ * - id/type/source/privacy <- note.metadata.aiwg_source
21
+ * - title/text <- note.title / note.original_content
22
+ * - tags <- note.tags
23
+ * - relationships <- links.jsonl (from_note_id/to_note_id/kind/score)
24
+ * - provenance/provenance_events <- provenance_activities.jsonl, split by
25
+ * the `source:<field>` activity_type convention this module uses to
26
+ * distinguish `provenance` entries from `provenance_events` entries
27
+ *
28
+ * The index-level wrapper (`AiwgFortemiIndexExport.source`/`compatibility`)
29
+ * is genuinely not preserved anywhere in a full-v1 archive -- there is no
30
+ * per-archive metadata sidecar for it, only per-note. Recovery reports this
31
+ * honestly with a fixed placeholder rather than inventing a value.
32
+ */
33
+ import { unpackTarGz } from '@fortemi/core';
34
+ import { EXPORT_MANIFEST_RECORD_TYPE, SESSION_EXPORT_INDEX_SCHEMA_VERSION, SESSION_EXPORT_RECORD_SCHEMA_VERSION, } from './fortemi-export-mapping.js';
35
+ const SOURCE_PROVENANCE_PREFIX = 'source:';
36
+ function decodeJsonLines(bytes) {
37
+ if (!bytes)
38
+ return [];
39
+ const text = new TextDecoder().decode(bytes);
40
+ return text.split('\n').filter((line) => line.trim().length > 0).map((line) => JSON.parse(line));
41
+ }
42
+ function isAiwgSourceSidecar(value) {
43
+ if (!value || typeof value !== 'object')
44
+ return false;
45
+ const candidate = value;
46
+ return typeof candidate.record_id === 'string' && typeof candidate.record_type === 'string';
47
+ }
48
+ export class FortemiShardRecoveryError extends Error {
49
+ }
50
+ /**
51
+ * Recovers the record graph from a `full-v1` archive. Throws
52
+ * `FortemiShardRecoveryError` if the archive has no notes, or any note is
53
+ * missing the `aiwg_source` sidecar this module's own mapping always writes
54
+ * (meaning the shard wasn't produced by this mapping and can't be recovered
55
+ * this way).
56
+ */
57
+ export function recoverAiwgFortemiIndexFromFullV1Shard(bytes) {
58
+ const files = unpackTarGz(bytes);
59
+ const notes = decodeJsonLines(files.get('notes.jsonl'));
60
+ if (notes.length === 0) {
61
+ throw new FortemiShardRecoveryError('Knowledge Shard contains no notes to recover');
62
+ }
63
+ const links = decodeJsonLines(files.get('links.jsonl'));
64
+ const activities = decodeJsonLines(files.get('provenance_activities.jsonl'));
65
+ const noteIdToRecordId = new Map();
66
+ for (const note of notes) {
67
+ const sidecar = note.metadata?.aiwg_source;
68
+ if (!isAiwgSourceSidecar(sidecar)) {
69
+ throw new FortemiShardRecoveryError(`Knowledge Shard note ${note.id} has no aiwg_source metadata -- `
70
+ + 'this archive was not produced by the AIWG session export mapping');
71
+ }
72
+ noteIdToRecordId.set(note.id, sidecar.record_id);
73
+ }
74
+ const items = notes.map((note) => {
75
+ const sidecar = note.metadata.aiwg_source;
76
+ const relationships = links
77
+ .filter((link) => link.from_note_id === note.id)
78
+ .map((link) => ({
79
+ type: link.kind,
80
+ target_id: link.to_note_id ? (noteIdToRecordId.get(link.to_note_id) ?? link.to_note_id) : '',
81
+ confidence: link.score ?? undefined,
82
+ }));
83
+ const noteActivities = activities.filter((activity) => activity.note_id === note.id);
84
+ const provenance = [];
85
+ const provenanceEvents = [];
86
+ for (const activity of noteActivities) {
87
+ const metadata = activity.metadata ?? {};
88
+ if (activity.activity_type.startsWith(SOURCE_PROVENANCE_PREFIX)) {
89
+ provenance.push({
90
+ field: activity.activity_type.slice(SOURCE_PROVENANCE_PREFIX.length),
91
+ source: metadata.source ?? activity.model_name ?? '',
92
+ path: metadata.path ?? '',
93
+ confidence: metadata.confidence ?? 'source',
94
+ privacy: metadata.privacy ?? 'private',
95
+ });
96
+ }
97
+ else {
98
+ provenanceEvents.push({
99
+ activity: activity.activity_type,
100
+ agent: activity.model_name ?? undefined,
101
+ started_at: activity.started_at ?? undefined,
102
+ ended_at: activity.ended_at ?? undefined,
103
+ source: metadata.source ?? undefined,
104
+ path: metadata.path ?? undefined,
105
+ confidence: metadata.confidence ?? undefined,
106
+ privacy: metadata.privacy ?? undefined,
107
+ attributes: metadata.attributes ?? undefined,
108
+ });
109
+ }
110
+ }
111
+ return {
112
+ schema_version: SESSION_EXPORT_RECORD_SCHEMA_VERSION,
113
+ id: sidecar.record_id,
114
+ type: sidecar.record_type,
115
+ source: sidecar.source,
116
+ title: note.title,
117
+ text: note.original_content,
118
+ facets: {},
119
+ tags: note.tags,
120
+ concepts: [],
121
+ relationships,
122
+ provenance,
123
+ provenance_events: provenanceEvents,
124
+ privacy: sidecar.privacy,
125
+ updated_at: sidecar.source.updated_at ?? new Date(0).toISOString(),
126
+ };
127
+ });
128
+ items.sort((left, right) => left.id.localeCompare(right.id));
129
+ // The export-manifest record (see fortemi-export-mapping.ts) carries the
130
+ // index-level wrapper fields through the same provenance_events mechanism
131
+ // as everything else. Extract it and exclude it from the recovered items
132
+ // -- it isn't a session, event, or output record.
133
+ const manifestIndex = items.findIndex((item) => item.type === EXPORT_MANIFEST_RECORD_TYPE);
134
+ const manifestAttributes = manifestIndex >= 0
135
+ ? items[manifestIndex].provenance_events?.[0]?.attributes
136
+ : undefined;
137
+ const recoveredItems = manifestIndex >= 0
138
+ ? items.filter((_, index) => index !== manifestIndex)
139
+ : items;
140
+ return {
141
+ schema_version: SESSION_EXPORT_INDEX_SCHEMA_VERSION,
142
+ generated_at: manifestAttributes?.generatedAt ?? new Date(0).toISOString(),
143
+ source: manifestAttributes
144
+ ? { repo: manifestAttributes.repo ?? 'unknown', privacy: manifestAttributes.privacy ?? 'private' }
145
+ // No manifest record present (e.g. an archive built before this record
146
+ // existed) -- labeled honestly rather than invented.
147
+ : { repo: 'recovered-from-full-v1-shard', privacy: 'private' },
148
+ items: recoveredItems,
149
+ };
150
+ }
151
+ //# sourceMappingURL=fortemi-shard-recovery.js.map
@@ -7,6 +7,11 @@ export * from './repository.js';
7
7
  export * from './ports.js';
8
8
  export * from './optional-backends.js';
9
9
  export * from './knowledge-shard.js';
10
+ export * from './fortemi-export-mapping.js';
11
+ export * from './fortemi-shard-recovery.js';
12
+ export * from './fortemi-shard-blobs.js';
13
+ export * from './output-lineage.js';
14
+ export * from './export-plan.js';
10
15
  export * from './candidates.js';
11
16
  export * from './promotion.js';
12
17
  export * from './output-registration.js';
@@ -28,6 +33,7 @@ export * from './adapters/hermes.js';
28
33
  export * from './adapters/opencode.js';
29
34
  export * from './adapters/openclaw.js';
30
35
  export * from './adapters/openhuman.js';
36
+ export * from './adapters/grokbot.js';
31
37
  export * from './adapters/pi.js';
32
38
  export * from './adapters/deepseek-harness.js';
33
39
  export * from './adapters/warp.js';
@@ -0,0 +1,43 @@
1
+ import { sessionRecordLocator } from './fortemi-export-mapping.js';
2
+ /** Known, exact conventions a `kind: 'session'` source ref may use to name a session. Order is the preference used to report `matchReason`. */
3
+ function sessionRefCandidates(session) {
4
+ return [
5
+ { ref: session.sessionId, reason: 'ref equals the stable session id' },
6
+ { ref: `session:${session.sessionId}`, reason: "ref equals 'session:<stable session id>'" },
7
+ { ref: sessionRecordLocator(session), reason: 'ref equals the aiwg-session:// export locator' },
8
+ { ref: session.nativeSessionId, reason: 'ref equals the native session id' },
9
+ { ref: `session:${session.nativeSessionId}`, reason: "ref equals 'session:<native session id>'" },
10
+ ];
11
+ }
12
+ function matchSource(source, session) {
13
+ if (source.kind !== 'session')
14
+ return null;
15
+ for (const candidate of sessionRefCandidates(session)) {
16
+ if (source.ref === candidate.ref)
17
+ return candidate.reason;
18
+ }
19
+ return null;
20
+ }
21
+ /**
22
+ * Registrations are scanned once and matched against every selected session
23
+ * -- bounded by the selection, not an unbounded corpus walk, satisfying
24
+ * "bound any file discovery to an explicit workspace/source scope."
25
+ */
26
+ export function discoverSessionOutputCandidates(registrations, sessions) {
27
+ const candidates = [];
28
+ for (const registration of registrations) {
29
+ for (const session of sessions) {
30
+ for (const source of registration.contextPack.sources) {
31
+ const reason = matchSource(source, session);
32
+ if (reason) {
33
+ candidates.push({
34
+ registration, sessionId: session.sessionId, lineage: 'registered', matchReason: reason,
35
+ });
36
+ break;
37
+ }
38
+ }
39
+ }
40
+ }
41
+ return candidates;
42
+ }
43
+ //# sourceMappingURL=output-lineage.js.map
@@ -16,7 +16,7 @@ import { redactSourceLocator } from './discovery.js';
16
16
  import { fingerprintSourceFile } from './readers.js';
17
17
  export const DISCOVERY_MANIFEST_VERSION = '1.0.0';
18
18
  const MANUAL_EXPORT_PROVIDERS = new Set([
19
- 'copilot', 'hermes', 'opencode', 'openclaw', 'openhuman',
19
+ 'copilot', 'hermes', 'opencode', 'openclaw', 'openhuman', 'grokbot',
20
20
  'warp', 'devin-desktop', 'generic',
21
21
  ]);
22
22
  export async function discoverWorkspaceHistories(options) {
@@ -16,6 +16,7 @@ import { AGENT_SKILLS_SIDECAR_SCHEMA, AIWG_SKILL_CONTROL_FIELDS, createAgentSkil
16
16
  import { getImportedAgentSkill } from './importer.js';
17
17
  import { validateAgentSkillContent } from './validator.js';
18
18
  import { resolveHermesHomePath } from '../providers/hermes-home.js';
19
+ import { GROKBOT_SKILLS_DIR_ENV, grokbotMissingRootRemediation, resolveGrokbotSkillsDir, } from '../providers/grokbot-paths.js';
19
20
  export const AGENT_SKILL_MANAGED_MARKER = '.aiwg-managed';
20
21
  export const AGENT_SKILL_DEPLOYMENT_SIDECAR = '.aiwg-agent-skill.json';
21
22
  const MARKER_CONTENT = 'aiwg-agent-skill-v1\n';
@@ -78,6 +79,22 @@ function resolvePolicy(target, options) {
78
79
  appendToDescription = namespace.appendToDescription;
79
80
  reasons.push('applies the Factory description guidance before strict validation');
80
81
  break;
82
+ case 'grokbot': {
83
+ // Fail closed: never invent ~/grokbot-skills or ~/.grokbot.
84
+ const configured = resolveGrokbotSkillsDir(process.env);
85
+ if (!configured) {
86
+ root = '';
87
+ status = 'unsupported';
88
+ supported = false;
89
+ reasons.push(grokbotMissingRootRemediation(process.env));
90
+ warnings.push(`Agent Skills deploy for grokbot requires absolute ${GROKBOT_SKILLS_DIR_ENV}; no filesystem root was invented`);
91
+ }
92
+ else {
93
+ root = configured;
94
+ reasons.push(`uses the operator-configured ${GROKBOT_SKILLS_DIR_ENV} skills root with strict managed ownership markers`);
95
+ }
96
+ break;
97
+ }
81
98
  case 'hermes':
82
99
  if (options.homeDir === undefined) {
83
100
  root = resolveHermesHomePath('skills');
@@ -36,6 +36,7 @@ const PLATFORM_BUILTINS = {
36
36
  'opencode': ['help', 'run'],
37
37
  'warp': ['help', 'settings'],
38
38
  'hermes': [],
39
+ 'grokbot': [],
39
40
  'openclaw': [],
40
41
  'openhuman': [],
41
42
  'factory': [],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiwg/cli",
3
- "version": "2026.9.12",
3
+ "version": "2026.9.16",
4
4
  "description": "Lightweight AIWG CLI for signed, versioned web-backed resources.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -20,7 +20,7 @@
20
20
  * --rules-only Deploy only rules (skip agents)
21
21
  * --dry-run Show what would be deployed without writing
22
22
  * --force Overwrite existing files
23
- * --provider <name> Target provider: antigravity (agy), claude (default), openai, codex, cursor, opencode, copilot, factory, pi, omp, deepseek-harness (dsh), warp, devin, hermes, or openclaw
23
+ * --provider <name> Target provider: antigravity (agy), claude (default), openai, codex, cursor, opencode, copilot, factory, grokbot, pi, omp, deepseek-harness (dsh), warp, devin, hermes, openhuman, or openclaw
24
24
  * --model <name> Override model for all tiers (blanket)
25
25
  * --reasoning-model <name> Override model for reasoning tasks
26
26
  * --coding-model <name> Override model for coding tasks
@@ -123,7 +123,7 @@ const PROVIDER_ALIASES = {
123
123
  'dsh': 'deepseek-harness',
124
124
  };
125
125
 
126
- const AVAILABLE_PROVIDERS = ['antigravity', 'claude', 'factory', 'codex', 'opencode', 'copilot', 'cursor', 'pi', 'omp', 'deepseek-harness', 'warp', 'windsurf', 'hermes', 'openclaw', 'openhuman'];
126
+ const AVAILABLE_PROVIDERS = ['antigravity', 'claude', 'factory', 'codex', 'opencode', 'copilot', 'cursor', 'pi', 'omp', 'deepseek-harness', 'warp', 'windsurf', 'hermes', 'openclaw', 'openhuman', 'grokbot'];
127
127
 
128
128
  const UNSUPPORTED_PROVIDER_HINTS = {
129
129
  'devin-cli': [
@@ -651,6 +651,7 @@ Providers (all deploy agents, commands, skills, and rules):
651
651
  Paths: AGENTS.md, .agents/skills/, .dsh/aiwg.cordis.patch.yml
652
652
  devin - Devin Desktop (preferred; aliases: devin-desktop, windsurf)
653
653
  Paths: .windsurf/agents/, .windsurf/workflows/, .windsurf/skills/, .windsurf/rules/
654
+ grokbot - Grok Bot (experimental; AGENTS.md bridge; skills only with AIWG_GROKBOT_SKILLS_DIR)
654
655
  hermes - Hermes Agent (MCP-based integration)
655
656
  Skills: $HERMES_HOME/skills/ (user-global; defaults to ~/.hermes/skills/) | Agents: AGENTS.md
656
657
  Commands/Rules: served via MCP, not file-deployed
@@ -0,0 +1,285 @@
1
+ /**
2
+ * Grok Bot provider (experimental).
3
+ *
4
+ * Discover-first project bridge + fail-closed user skill root.
5
+ * Never targets `.cursor/**` or invents `~/.grokbot`.
6
+ *
7
+ * User-scope skill writes require AIWG_GROKBOT_SKILLS_DIR (absolute path).
8
+ * See `docs/architecture/adr-grokbot-provider-target.md` and #203–#208.
9
+ */
10
+
11
+ import path from 'path';
12
+ import os from 'os';
13
+ import {
14
+ collectFrameworkArtifacts,
15
+ createAgentsMdFromTemplate,
16
+ deploySkillsWithKernelRouting,
17
+ getAddonSkillDirs,
18
+ normalizeDeploymentMode,
19
+ resolveAiwgRoot,
20
+ } from './base.mjs';
21
+
22
+ export const name = 'grokbot';
23
+ export const aliases = [];
24
+
25
+ const SKILLS_DIR_ENV = 'AIWG_GROKBOT_SKILLS_DIR';
26
+
27
+ /**
28
+ * Resolve configured Grok Bot skills directory (fail-closed).
29
+ * Mirrors src/providers/grokbot-paths.ts for the JS deploy path.
30
+ */
31
+ export function resolveGrokbotSkillsDir(env = process.env, userHome = os.homedir()) {
32
+ const raw = (env[SKILLS_DIR_ENV] || '').trim();
33
+ if (!raw) return null;
34
+ let expanded = raw;
35
+ if (raw === '~') return null;
36
+ if (raw.startsWith('~/')) expanded = path.join(userHome, raw.slice(2));
37
+ if (!path.isAbsolute(expanded)) return null;
38
+ const resolved = path.resolve(expanded);
39
+ if (!resolved || resolved === path.sep) return null;
40
+ return resolved;
41
+ }
42
+
43
+ export function getGrokbotMissingRootMessage(env = process.env) {
44
+ const raw = (env[SKILLS_DIR_ENV] || '').trim();
45
+ if (!raw) {
46
+ return (
47
+ `Grok Bot user-scope skill root is not configured. Set ${SKILLS_DIR_ENV} ` +
48
+ 'to the absolute path of the Grok Bot skills/workflows directory before ' +
49
+ 'using --scope user or --global. AIWG does not invent ~/.grokbot.'
50
+ );
51
+ }
52
+ if (raw === '~' || (!raw.startsWith('~/') && !path.isAbsolute(raw))) {
53
+ return (
54
+ `${SKILLS_DIR_ENV} must be an absolute path (got '${raw}'). ` +
55
+ 'Relative values and bare ~ are rejected.'
56
+ );
57
+ }
58
+ if (raw.startsWith('~/')) {
59
+ const expanded = path.join(os.homedir(), raw.slice(2));
60
+ if (!path.isAbsolute(path.resolve(expanded))) {
61
+ return `${SKILLS_DIR_ENV} must resolve to an absolute path (got '${raw}').`;
62
+ }
63
+ }
64
+ return (
65
+ `Grok Bot skill root '${raw}' is not usable. Set ${SKILLS_DIR_ENV} to a ` +
66
+ 'valid absolute directory.'
67
+ );
68
+ }
69
+
70
+ // Project-relative paths stay empty: agents/commands/rules are indexed.
71
+ // Skills path is absolute only when the operator configured the env override;
72
+ // otherwise '' so project deploy skips native skill copies.
73
+ function configuredSkillsPath() {
74
+ return resolveGrokbotSkillsDir() || '';
75
+ }
76
+
77
+ export const paths = {
78
+ agents: '',
79
+ commands: '',
80
+ get skills() {
81
+ return configuredSkillsPath();
82
+ },
83
+ rules: '',
84
+ };
85
+
86
+ // Empty when unset — deploySkills resolves AIWG_GROKBOT_SKILLS_DIR dynamically.
87
+ // Never default to ~/.grokbot.
88
+ export const kernelSkillsPath = '';
89
+
90
+ export const support = {
91
+ agents: 'indexed',
92
+ commands: 'indexed',
93
+ skills: 'native-when-configured',
94
+ rules: 'indexed',
95
+ };
96
+
97
+ export const capabilities = {
98
+ skills: true,
99
+ rules: false,
100
+ aggregatedOutput: false,
101
+ yamlFormat: false,
102
+ homeDirectoryDeploy: true,
103
+ };
104
+
105
+ export function mapModel(originalModel) {
106
+ return originalModel;
107
+ }
108
+
109
+ export function transformAgent(_srcPath, content) {
110
+ return content;
111
+ }
112
+
113
+ export function transformCommand(_srcPath, content) {
114
+ return content;
115
+ }
116
+
117
+ export function deployAgents() {
118
+ return 0;
119
+ }
120
+
121
+ export function deployCommands() {
122
+ return 0;
123
+ }
124
+
125
+ export function deployRules() {
126
+ return 0;
127
+ }
128
+
129
+ /**
130
+ * Deploy skills only when AIWG_GROKBOT_SKILLS_DIR is configured.
131
+ * Kernel → configured root; standard → index/discovery unless --copy-all
132
+ * (then `<root>/.aiwg/skills`).
133
+ */
134
+ export function deploySkills(skillDirs, targetDir, opts = {}) {
135
+ const root = resolveGrokbotSkillsDir(opts.env || process.env);
136
+ if (!root) {
137
+ if (!opts.quiet) {
138
+ console.log(
139
+ `\n Grok Bot: skipping native skill copy (${SKILLS_DIR_ENV} unset). ` +
140
+ 'Skills remain available via aiwg discover / aiwg show.',
141
+ );
142
+ }
143
+ return 0;
144
+ }
145
+
146
+ if (root.includes(`${path.sep}.cursor${path.sep}`) || root.endsWith(`${path.sep}.cursor`) || root.includes('/.cursor/') || root.endsWith('/.cursor')) {
147
+ throw new Error(
148
+ `Refusing to deploy Grok Bot skills into a .cursor path (${root}). ` +
149
+ `Set ${SKILLS_DIR_ENV} to the Grok Bot skills root, not Cursor.`,
150
+ );
151
+ }
152
+
153
+ const kernelDest = root;
154
+ const standardDest = path.join(root, '.aiwg', 'skills');
155
+ if (!opts.quiet) {
156
+ console.log(
157
+ `\n Grok Bot skills root: ${root} (from ${SKILLS_DIR_ENV})` +
158
+ (opts.dryRun ? ' [dry-run]' : ''),
159
+ );
160
+ }
161
+ return deploySkillsWithKernelRouting(skillDirs, standardDest, kernelDest, {
162
+ ...opts,
163
+ copyStandardSkills: opts.copyStandardSkills === true,
164
+ });
165
+ }
166
+
167
+ export function createAgentsMd(target, srcRoot, dryRun) {
168
+ const aiwgRoot = resolveAiwgRoot(srcRoot) || srcRoot;
169
+ createAgentsMdFromTemplate(target, aiwgRoot, 'grokbot/AGENTS.md.aiwg-template', dryRun);
170
+ }
171
+
172
+ export async function postDeploy(targetDir, opts = {}) {
173
+ if (
174
+ opts.createAgentsMd ||
175
+ (!opts.commandsOnly && !opts.skillsOnly && !opts.rulesOnly)
176
+ ) {
177
+ createAgentsMd(targetDir, opts.srcRoot, opts.dryRun);
178
+ }
179
+ if (!opts.quiet) {
180
+ const root = resolveGrokbotSkillsDir(opts.env || process.env);
181
+ if (root) {
182
+ console.log(
183
+ 'Grok Bot: after deploy, start a new agent chat or re-read skills. ' +
184
+ 'Live refresh is not claimed until product behavior is verified.',
185
+ );
186
+ } else {
187
+ console.log(
188
+ `Grok Bot: project bridge only. Set ${SKILLS_DIR_ENV} for user-scope skill deploy.`,
189
+ );
190
+ }
191
+ }
192
+ }
193
+
194
+ export function getFileExtension() {
195
+ return '.md';
196
+ }
197
+
198
+ /**
199
+ * Assert user-scope is allowed. Called when the orchestrator is mirroring
200
+ * to user scope or when opts.scope === 'user'.
201
+ */
202
+ export function assertUserScopeConfigured(opts = {}) {
203
+ const root = resolveGrokbotSkillsDir(opts.env || process.env);
204
+ if (!root) {
205
+ const err = new Error(getGrokbotMissingRootMessage(opts.env || process.env));
206
+ err.code = 'GROKBOT_SKILLS_DIR_UNSET';
207
+ throw err;
208
+ }
209
+ return root;
210
+ }
211
+
212
+ export async function deploy(opts = {}) {
213
+ const {
214
+ srcRoot,
215
+ target,
216
+ mode,
217
+ deploySkills: shouldDeploySkills,
218
+ skillsOnly,
219
+ commandsOnly,
220
+ rulesOnly,
221
+ scope,
222
+ } = opts;
223
+
224
+ if (!opts.quiet) {
225
+ console.log(`\n=== Grok Bot Provider (experimental) ===`);
226
+ console.log(`Target: ${target}`);
227
+ console.log(`Mode: ${mode}`);
228
+ const root = resolveGrokbotSkillsDir(opts.env || process.env);
229
+ console.log(
230
+ `Skills root: ${root || `(unset — set ${SKILLS_DIR_ENV} for user-scope writes)`}`,
231
+ );
232
+ if (opts.dryRun) console.log('Dry-run: no filesystem mutations');
233
+ }
234
+
235
+ // Fail closed when caller explicitly requested user scope without a root.
236
+ if (scope === 'user' || opts.userScope === true) {
237
+ assertUserScopeConfigured(opts);
238
+ }
239
+
240
+ const skillDirs = [];
241
+ const normalizedMode = normalizeDeploymentMode(mode);
242
+
243
+ if (['general', 'sdlc', 'both', 'all'].includes(normalizedMode)) {
244
+ if (shouldDeploySkills || skillsOnly) skillDirs.push(...getAddonSkillDirs(srcRoot));
245
+ }
246
+
247
+ const fw = collectFrameworkArtifacts(srcRoot, normalizedMode, {
248
+ includeAgents: false,
249
+ includeCommands: false,
250
+ includeSkills: shouldDeploySkills || skillsOnly,
251
+ includeRules: false,
252
+ });
253
+ skillDirs.push(...fw.skills);
254
+
255
+ let count = 0;
256
+ if ((shouldDeploySkills || skillsOnly) && !commandsOnly && !rulesOnly) {
257
+ count += deploySkills(skillDirs, target, opts);
258
+ }
259
+
260
+ await postDeploy(target, opts);
261
+ return count;
262
+ }
263
+
264
+ export default {
265
+ name,
266
+ aliases,
267
+ paths,
268
+ kernelSkillsPath,
269
+ support,
270
+ capabilities,
271
+ mapModel,
272
+ transformAgent,
273
+ transformCommand,
274
+ deployAgents,
275
+ deployCommands,
276
+ deploySkills,
277
+ deployRules,
278
+ createAgentsMd,
279
+ postDeploy,
280
+ getFileExtension,
281
+ deploy,
282
+ resolveGrokbotSkillsDir,
283
+ getGrokbotMissingRootMessage,
284
+ assertUserScopeConfigured,
285
+ };