impulso 0.19.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +425 -0
- package/agents/ImpulsoOrchestrator.md +112 -0
- package/agents/debugger.md +38 -0
- package/agents/generalist.md +26 -0
- package/agents/implementer-frontend.md +41 -0
- package/agents/implementer-infra.md +41 -0
- package/agents/implementer.md +35 -0
- package/agents/planner.md +52 -0
- package/agents/reviewer.md +40 -0
- package/agents/scout-writer.md +24 -0
- package/agents/scout.md +22 -0
- package/agents/shipper.md +64 -0
- package/commands/impulso-address-pr-comments.md +162 -0
- package/commands/impulso-brainstorm.md +54 -0
- package/commands/impulso-context-pack.md +55 -0
- package/commands/impulso-debug.md +50 -0
- package/commands/impulso-directspeech-compress.md +137 -0
- package/commands/impulso-directspeech-help.md +33 -0
- package/commands/impulso-directspeech-stats.md +16 -0
- package/commands/impulso-implement.md +86 -0
- package/commands/impulso-plan.md +92 -0
- package/commands/impulso-ship.md +46 -0
- package/commands/impulso-spec.md +106 -0
- package/commands/impulso-verify.md +99 -0
- package/commands/plannotator-annotate.md +47 -0
- package/commands/plannotator-last.md +45 -0
- package/commands/plannotator-review.md +46 -0
- package/harnesses/claude/README.md +109 -0
- package/harnesses/claude/hooks/bash-state-guard.cjs +84 -0
- package/harnesses/claude/hooks/directspeech-context.cjs +29 -0
- package/harnesses/claude/hooks/path-guard.cjs +106 -0
- package/harnesses/claude/hooks/preflight.cjs +39 -0
- package/harnesses/claude/hooks/pretooluse-bash.cjs +69 -0
- package/harnesses/claude/hooks/rtk.cjs +87 -0
- package/harnesses/claude/hooks/subagent-start.cjs +83 -0
- package/harnesses/claude/hooks/task-exit.cjs +189 -0
- package/harnesses/claude/hooks/tdd-observer.cjs +118 -0
- package/harnesses/claude/hooks/tokensave-stale.cjs +63 -0
- package/harnesses/claude/hooks/userpromptsubmit.cjs +42 -0
- package/harnesses/claude/install.sh +120 -0
- package/harnesses/claude/merge-settings.cjs +71 -0
- package/harnesses/claude/uninstall.sh +89 -0
- package/harnesses/opencode/bin/darwin-amd64/impulso-hub +0 -0
- package/harnesses/opencode/bin/darwin-arm64/impulso-hub +0 -0
- package/harnesses/opencode/bin/impulso-hub +0 -0
- package/harnesses/opencode/bin/linux-amd64/impulso-hub +0 -0
- package/harnesses/opencode/bin/linux-arm64/impulso-hub +0 -0
- package/harnesses/opencode/commands-loader.js +101 -0
- package/harnesses/opencode/compose.js +111 -0
- package/harnesses/opencode/directspeech/README.md +43 -0
- package/harnesses/opencode/directspeech/plugin.js +47 -0
- package/harnesses/opencode/frontmatter.js +60 -0
- package/harnesses/opencode/install.sh +121 -0
- package/harnesses/opencode/orchestration/index.js +362 -0
- package/harnesses/opencode/plugin.js +84 -0
- package/harnesses/opencode/rtk/README.md +100 -0
- package/harnesses/opencode/rtk/rtk.ts +41 -0
- package/harnesses/opencode/session-name/plugin.js +27 -0
- package/harnesses/opencode/sync-commands.js +98 -0
- package/harnesses/opencode/uninstall.sh +36 -0
- package/opencode/opencode.jsonc +35 -0
- package/package.json +95 -0
- package/scripts/bump-plugin-version.cjs +28 -0
- package/scripts/install-hub.js +78 -0
- package/scripts/sync-agents.cjs +167 -0
- package/shared/directspeech-config.cjs +18 -0
- package/shared/orchestration-config.cjs +281 -0
- package/shared/orchestration-data.json +48 -0
- package/shared/state-guard.cjs +56 -0
- package/shared/tokensave-freshness.cjs +62 -0
- package/skills/impulso-address-pr-comments/SKILL.md +159 -0
- package/skills/impulso-brainstorm/SKILL.md +51 -0
- package/skills/impulso-context-pack/SKILL.md +52 -0
- package/skills/impulso-debug/SKILL.md +47 -0
- package/skills/impulso-directspeech-compress/SKILL.md +134 -0
- package/skills/impulso-directspeech-help/SKILL.md +30 -0
- package/skills/impulso-directspeech-stats/SKILL.md +13 -0
- package/skills/impulso-implement/SKILL.md +83 -0
- package/skills/impulso-plan/SKILL.md +89 -0
- package/skills/impulso-ship/SKILL.md +43 -0
- package/skills/impulso-spec/SKILL.md +103 -0
- package/skills/impulso-verify/SKILL.md +96 -0
- package/skills/plannotator-annotate/SKILL.md +45 -0
- package/skills/plannotator-last/SKILL.md +43 -0
- package/skills/plannotator-review/SKILL.md +44 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# Impulso claude uninstall — reverses install.sh. Targeted removals only:
|
|
5
|
+
# strips impulso hooks from settings.json, removes the ruleset file and its
|
|
6
|
+
# CLAUDE.md import line, removes only repo-owned symlinks, deregisters the
|
|
7
|
+
# tokensave MCP. Foreign content is never touched. Never rm -rf's ~/.claude.
|
|
8
|
+
#
|
|
9
|
+
# Default profile dir: ~/.config/impulso-profile/claude
|
|
10
|
+
# Override with: IMPULSO_CLAUDE_PROFILE=/some/dir ./uninstall.sh
|
|
11
|
+
|
|
12
|
+
REPO="$(cd "$(dirname "$0")/../.." && pwd)"
|
|
13
|
+
DEFAULT_PROFILE="${HOME}/.config/impulso-profile/claude"
|
|
14
|
+
PROFILE_DIR="${IMPULSO_CLAUDE_PROFILE:-${DEFAULT_PROFILE}}"
|
|
15
|
+
|
|
16
|
+
echo "==> Impulso uninstall (claude)"
|
|
17
|
+
echo " Profile: ${PROFILE_DIR}"
|
|
18
|
+
|
|
19
|
+
if [[ ! -d "${PROFILE_DIR}" ]]; then
|
|
20
|
+
echo " Nothing to do — profile dir does not exist."
|
|
21
|
+
exit 0
|
|
22
|
+
fi
|
|
23
|
+
|
|
24
|
+
# Deregister tokensave MCP (user scope in this profile).
|
|
25
|
+
if command -v claude >/dev/null 2>&1 \
|
|
26
|
+
&& CLAUDE_CONFIG_DIR="${PROFILE_DIR}" claude mcp get tokensave >/dev/null 2>&1; then
|
|
27
|
+
CLAUDE_CONFIG_DIR="${PROFILE_DIR}" claude mcp remove --scope user tokensave >/dev/null 2>&1 || true
|
|
28
|
+
echo " Deregistered tokensave MCP"
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
# Strip impulso hooks from settings.json (foreign hooks/keys survive).
|
|
32
|
+
if [[ -f "${PROFILE_DIR}/settings.json" ]]; then
|
|
33
|
+
node "${REPO}/harnesses/claude/merge-settings.cjs" --remove "${PROFILE_DIR}/settings.json"
|
|
34
|
+
echo " Removed impulso hooks from settings.json"
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
# Ruleset + CLAUDE.md import line.
|
|
38
|
+
rm -f "${PROFILE_DIR}/IMPULSO-AGENTS.md" "${PROFILE_DIR}/AGENTS.md"
|
|
39
|
+
if [[ -f "${PROFILE_DIR}/CLAUDE.md" ]]; then
|
|
40
|
+
grep -vF '@IMPULSO-AGENTS.md' "${PROFILE_DIR}/CLAUDE.md" | grep -vFx '@AGENTS.md' > "${PROFILE_DIR}/CLAUDE.md.tmp" || true
|
|
41
|
+
if [[ -s "${PROFILE_DIR}/CLAUDE.md.tmp" ]] && grep -q '[^[:space:]]' "${PROFILE_DIR}/CLAUDE.md.tmp"; then
|
|
42
|
+
mv "${PROFILE_DIR}/CLAUDE.md.tmp" "${PROFILE_DIR}/CLAUDE.md"
|
|
43
|
+
echo " Removed import line from CLAUDE.md"
|
|
44
|
+
else
|
|
45
|
+
rm -f "${PROFILE_DIR}/CLAUDE.md.tmp" "${PROFILE_DIR}/CLAUDE.md"
|
|
46
|
+
echo " Removed CLAUDE.md (contained only the impulso import)"
|
|
47
|
+
fi
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
# skills/, commands/, and agents/: remove only symlinks pointing into this repo.
|
|
51
|
+
for sub in skills commands agents; do
|
|
52
|
+
p="${PROFILE_DIR}/${sub}"
|
|
53
|
+
if [[ -L "$p" && "$(readlink "$p")" == "${REPO}/"* ]]; then
|
|
54
|
+
rm "$p"
|
|
55
|
+
echo " Removed ${sub} symlink"
|
|
56
|
+
elif [[ -d "$p" ]]; then
|
|
57
|
+
for entry in "$p"/*; do
|
|
58
|
+
[[ -L "$entry" && "$(readlink "$entry")" == "${REPO}/"* ]] && rm "$entry"
|
|
59
|
+
done
|
|
60
|
+
echo " Removed repo-owned entries from ${sub}/"
|
|
61
|
+
fi
|
|
62
|
+
done
|
|
63
|
+
|
|
64
|
+
# impulso-hub binary: remove if it exists.
|
|
65
|
+
GATE_DST="${HOME}/.local/bin/impulso-hub"
|
|
66
|
+
if [[ -f "${GATE_DST}" ]]; then
|
|
67
|
+
rm -f "${GATE_DST}"
|
|
68
|
+
echo " Removed impulso-hub binary"
|
|
69
|
+
|
|
70
|
+
# Remove old impulso-gate binary if present (renamed to impulso-hub)
|
|
71
|
+
OLD_GATE="${HOME}/.local/bin/impulso-gate"
|
|
72
|
+
if [ -f "${OLD_GATE}" ] || [ -L "${OLD_GATE}" ]; then
|
|
73
|
+
rm -f "${OLD_GATE}" && echo " Removed old impulso-gate binary"
|
|
74
|
+
fi
|
|
75
|
+
fi
|
|
76
|
+
# Also clean up stale symlink from old CJS installer
|
|
77
|
+
if [[ -L "${GATE_DST}" ]]; then
|
|
78
|
+
rm -f "${GATE_DST}"
|
|
79
|
+
echo " Removed stale impulso-hub symlink"
|
|
80
|
+
fi
|
|
81
|
+
|
|
82
|
+
# Only the default isolated profile dir is removed wholesale — and never
|
|
83
|
+
# ~/.claude, even if someone points IMPULSO_CLAUDE_PROFILE at it.
|
|
84
|
+
if [[ "${PROFILE_DIR}" == "${DEFAULT_PROFILE}" && "${PROFILE_DIR}" != "${HOME}/.claude" ]]; then
|
|
85
|
+
rm -rf "${PROFILE_DIR}"
|
|
86
|
+
echo " Removed isolated profile dir"
|
|
87
|
+
fi
|
|
88
|
+
|
|
89
|
+
echo "==> Done."
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// impulso — runtime commands loader
|
|
2
|
+
//
|
|
3
|
+
// Reads bundled commands/*.md files at plugin load time and registers each
|
|
4
|
+
// as a config.command[name] inline template. This avoids requiring users to
|
|
5
|
+
// symlink command files manually.
|
|
6
|
+
//
|
|
7
|
+
// Each command .md file has frontmatter (--- delimiters) with at minimum a
|
|
8
|
+
// description: field, followed by the body. The existing trailer
|
|
9
|
+
// ---
|
|
10
|
+
// **User arguments:** $ARGUMENTS
|
|
11
|
+
// is stripped from the body and re-appended, matching the format produced by
|
|
12
|
+
// sync-commands.js.
|
|
13
|
+
|
|
14
|
+
import fs from 'node:fs';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Parse a command .md file. Command frontmatter only has a description: field;
|
|
19
|
+
* the command name is derived from the filename (minus .md extension).
|
|
20
|
+
* @returns {{ description: string, body: string } | null}
|
|
21
|
+
*/
|
|
22
|
+
function parseCommandFile(content) {
|
|
23
|
+
const m = /^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/.exec(content);
|
|
24
|
+
if (!m) return null;
|
|
25
|
+
|
|
26
|
+
const raw = m[1];
|
|
27
|
+
const body = m[2];
|
|
28
|
+
|
|
29
|
+
const lines = raw.split(/\r?\n/);
|
|
30
|
+
let description = null;
|
|
31
|
+
|
|
32
|
+
for (let i = 0; i < lines.length; i++) {
|
|
33
|
+
const line = lines[i];
|
|
34
|
+
const descMatch = /^description:\s*(.*)$/.exec(line);
|
|
35
|
+
if (descMatch) {
|
|
36
|
+
const rest = descMatch[1].trim();
|
|
37
|
+
if (rest === '>' || rest === '|' || rest === '') {
|
|
38
|
+
const parts = [];
|
|
39
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
40
|
+
if (/^\s/.test(lines[j])) {
|
|
41
|
+
parts.push(lines[j].trim());
|
|
42
|
+
} else if (lines[j] === '') {
|
|
43
|
+
continue;
|
|
44
|
+
} else {
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
description = parts.filter(Boolean).join(' ');
|
|
49
|
+
} else {
|
|
50
|
+
// Strip quotes if prettier added them
|
|
51
|
+
description = rest.replace(/^['"]|['"]$/g, '');
|
|
52
|
+
}
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!description) return null;
|
|
58
|
+
return { description, body };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Read commands/*.md from the given directory and register each as an
|
|
63
|
+
* inline config.command entry.
|
|
64
|
+
* @param {object} config — the opencode Config object (mutated in place)
|
|
65
|
+
* @param {string} commandsDir — absolute path to the commands/ directory
|
|
66
|
+
*/
|
|
67
|
+
export function registerCommands(config, commandsDir) {
|
|
68
|
+
let files;
|
|
69
|
+
try {
|
|
70
|
+
files = fs.readdirSync(commandsDir).filter((f) => f.endsWith('.md'));
|
|
71
|
+
} catch (e) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
config.command = config.command || {};
|
|
76
|
+
|
|
77
|
+
for (const filename of files) {
|
|
78
|
+
const name = filename.replace(/\.md$/, '');
|
|
79
|
+
if (config.command[name]) continue;
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
const raw = fs.readFileSync(path.join(commandsDir, filename), 'utf8');
|
|
83
|
+
const parsed = parseCommandFile(raw);
|
|
84
|
+
if (!parsed) {
|
|
85
|
+
console.warn(`[impulso] failed to parse command ${filename}: missing description`);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const { description, body } = parsed;
|
|
90
|
+
const cleanedBody = body
|
|
91
|
+
.replace(/^---\r?\n\*\*User arguments:\*\* \$ARGUMENTS\s*$/m, '')
|
|
92
|
+
.replace(/\s+$/, '');
|
|
93
|
+
|
|
94
|
+
const template = cleanedBody + '\n\n---\n\n**User arguments:** $ARGUMENTS';
|
|
95
|
+
|
|
96
|
+
config.command[name] = { template, description };
|
|
97
|
+
} catch (e) {
|
|
98
|
+
console.warn(`[impulso] failed to register command ${filename}:`, e.message);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// impulso — plugin hook composer
|
|
2
|
+
//
|
|
3
|
+
// Merges an array of Hooks objects into one. For hook keys that accept
|
|
4
|
+
// (input, output) — e.g. chat.message, experimental.chat.system.transform,
|
|
5
|
+
// command.execute.before, tool.definition, experimental.chat.messages.transform —
|
|
6
|
+
// each sub-plugin's handler is called in sequence with the same input/output pair.
|
|
7
|
+
//
|
|
8
|
+
// For `event`, handlers are called sequentially with the event.
|
|
9
|
+
// For `config`, handlers are called sequentially mutating config in place.
|
|
10
|
+
// For `tool`, definitions are shallow-merged (last-write-wins per name).
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Hook keys that receive (input, output) pairs. Names match
|
|
14
|
+
* @opencode-ai/plugin Hooks interface.
|
|
15
|
+
*/
|
|
16
|
+
const OUTPUT_HOOKS = [
|
|
17
|
+
'chat.message',
|
|
18
|
+
'chat.params',
|
|
19
|
+
'chat.headers',
|
|
20
|
+
'experimental.chat.messages.transform',
|
|
21
|
+
'experimental.chat.system.transform',
|
|
22
|
+
'experimental.session.compacting',
|
|
23
|
+
'experimental.compaction.autocontinue',
|
|
24
|
+
'experimental.text.complete',
|
|
25
|
+
'experimental.provider.small_model',
|
|
26
|
+
'tool.definition',
|
|
27
|
+
'tool.execute.before',
|
|
28
|
+
'tool.execute.after',
|
|
29
|
+
'command.execute.before',
|
|
30
|
+
'shell.env',
|
|
31
|
+
'permission.ask',
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Hook keys that receive a single config object.
|
|
36
|
+
*/
|
|
37
|
+
const CONFIG_HOOKS = ['config'];
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Hook keys that receive a single input object.
|
|
41
|
+
*/
|
|
42
|
+
const INPUT_HOOKS = ['event'];
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Hook keys whose values are objects (merged via shallow assign rather than
|
|
46
|
+
* called in sequence).
|
|
47
|
+
*/
|
|
48
|
+
const MERGE_HOOKS = ['tool'];
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @param {Array<import('@opencode-ai/plugin').Hooks>} hooksList
|
|
52
|
+
* @returns {import('@opencode-ai/plugin').Hooks}
|
|
53
|
+
*/
|
|
54
|
+
export function composeHooks(hooksList) {
|
|
55
|
+
const merged = {};
|
|
56
|
+
|
|
57
|
+
// Collect handlers per strategy
|
|
58
|
+
const outputMap = {};
|
|
59
|
+
const inputList = [];
|
|
60
|
+
const configList = [];
|
|
61
|
+
const mergeMap = {};
|
|
62
|
+
|
|
63
|
+
for (const hooks of hooksList) {
|
|
64
|
+
for (const key of OUTPUT_HOOKS) {
|
|
65
|
+
if (hooks[key]) {
|
|
66
|
+
outputMap[key] = outputMap[key] || [];
|
|
67
|
+
outputMap[key].push(hooks[key]);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
for (const key of INPUT_HOOKS) {
|
|
71
|
+
if (hooks[key]) inputList.push(hooks[key]);
|
|
72
|
+
}
|
|
73
|
+
for (const key of CONFIG_HOOKS) {
|
|
74
|
+
if (hooks[key]) configList.push(hooks[key]);
|
|
75
|
+
}
|
|
76
|
+
for (const key of MERGE_HOOKS) {
|
|
77
|
+
if (hooks[key]) {
|
|
78
|
+
mergeMap[key] = mergeMap[key] || {};
|
|
79
|
+
Object.assign(mergeMap[key], hooks[key]);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Build callers for (input, output) hooks
|
|
85
|
+
for (const [key, handlers] of Object.entries(outputMap)) {
|
|
86
|
+
merged[key] = async (input, output) => {
|
|
87
|
+
for (const fn of handlers) await fn(input, output);
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Build caller for single-input hooks (event)
|
|
92
|
+
if (inputList.length > 0) {
|
|
93
|
+
merged.event = async (input) => {
|
|
94
|
+
for (const fn of inputList) await fn(input);
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Build caller for single-config hooks
|
|
99
|
+
if (configList.length > 0) {
|
|
100
|
+
merged.config = async (config) => {
|
|
101
|
+
for (const fn of configList) await fn(config);
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Merge object hooks (tool) into the result
|
|
106
|
+
for (const [key, obj] of Object.entries(mergeMap)) {
|
|
107
|
+
merged[key] = obj;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return merged;
|
|
111
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Impulso-DirectSpeech (opencode harness)
|
|
2
|
+
|
|
3
|
+
DirectSpeech is **always on**. No modes, no toggles, no flag files.
|
|
4
|
+
|
|
5
|
+
Two layers:
|
|
6
|
+
|
|
7
|
+
1. **Ruleset** — the Impulso mode block in `shared/AGENTS.md`, loaded into every
|
|
8
|
+
session via the profile `AGENTS.md`.
|
|
9
|
+
2. **Per-turn reinforcement** — this plugin pushes one line into the system
|
|
10
|
+
prompt each turn (`experimental.chat.system.transform` hook) so the style
|
|
11
|
+
survives long conversations and compaction:
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
IMPULSO DIRECT SPEECH MODE ACTIVE. Terse, technical, no filler. Fragments OK.
|
|
15
|
+
Code/commits/security: write normal.
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
The line lives in `shared/directspeech-config.cjs` — shared with the Claude Code
|
|
19
|
+
harness, which injects it via a UserPromptSubmit hook.
|
|
20
|
+
|
|
21
|
+
Escape hatches are model-level, not machinery: Auto-Clarity drops terseness for
|
|
22
|
+
security warnings and irreversible actions; code/commits/PRs are always written
|
|
23
|
+
normal.
|
|
24
|
+
|
|
25
|
+
## File overview
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
harnesses/opencode/directspeech/
|
|
29
|
+
└── plugin.js # opencode plugin (ESM, loaded via profile plugins array)
|
|
30
|
+
shared/
|
|
31
|
+
└── directspeech-config.cjs # reinforcement line (CJS, shared across harnesses)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
`directspeech-config.cjs` is loaded by hand (not via module loader) because the
|
|
35
|
+
opencode Bun binary rejects `require()` of on-disk files and `await import()` of
|
|
36
|
+
CJS yields an empty namespace. `plugin.js` evaluates it through `new Function()`
|
|
37
|
+
— a deliberate choice for this sibling file only, not a general mechanism.
|
|
38
|
+
|
|
39
|
+
## Skills relationship
|
|
40
|
+
|
|
41
|
+
The independent skills (`/impulso-directspeech-compress`, `-stats`, `-help`) are
|
|
42
|
+
regular bundled skills with their own slash commands — they don't interact with
|
|
43
|
+
the reinforcement line.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// impulso-directspeech — opencode plugin
|
|
2
|
+
//
|
|
3
|
+
// DirectSpeech is always on: inject the per-turn reinforcement line into the
|
|
4
|
+
// system prompt, unconditionally. No flags, no toggles, no modes. The full
|
|
5
|
+
// ruleset comes from the profile AGENTS.md; this is the drift guard.
|
|
6
|
+
//
|
|
7
|
+
// Bun ESM module; loads the shared reinforcement line from
|
|
8
|
+
// shared/directspeech-config.cjs via a Function-constructor CJS shim
|
|
9
|
+
// (the Bun binary rejects require() of on-disk CJS, and await import()
|
|
10
|
+
// of CJS yields an empty namespace).
|
|
11
|
+
//
|
|
12
|
+
// Hook mapping (opencode >= 1.15.x):
|
|
13
|
+
// - experimental.chat.system.transform: inject reinforcement
|
|
14
|
+
|
|
15
|
+
import { createRequire } from 'node:module';
|
|
16
|
+
import { fileURLToPath } from 'node:url';
|
|
17
|
+
import { dirname, join } from 'node:path';
|
|
18
|
+
import { readFileSync } from 'node:fs';
|
|
19
|
+
|
|
20
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
|
|
22
|
+
function loadConfig() {
|
|
23
|
+
const target = join(here, '../../../shared/directspeech-config.cjs');
|
|
24
|
+
const code = readFileSync(target, 'utf8').replace(/^#![^\n]*\n/, '');
|
|
25
|
+
const mod = { exports: {} };
|
|
26
|
+
new Function('module', 'exports', 'require', '__dirname', '__filename', code)(
|
|
27
|
+
mod,
|
|
28
|
+
mod.exports,
|
|
29
|
+
createRequire(import.meta.url),
|
|
30
|
+
dirname(target),
|
|
31
|
+
target,
|
|
32
|
+
);
|
|
33
|
+
return mod.exports;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const { reinforcementLine } = loadConfig();
|
|
37
|
+
|
|
38
|
+
export const ImpulsoDirectSpeechPlugin = async (_ctx) => {
|
|
39
|
+
return {
|
|
40
|
+
'experimental.chat.system.transform': async (_input, output) => {
|
|
41
|
+
if (!output || !Array.isArray(output.system)) return;
|
|
42
|
+
output.system.push(reinforcementLine());
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export default ImpulsoDirectSpeechPlugin;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Shared frontmatter parser — used by sync-commands.js (build-time) and
|
|
2
|
+
// commands-loader.js (runtime plugin).
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Parse YAML frontmatter from a SKILL.md or command .md file.
|
|
6
|
+
* Returns { name, description, body }.
|
|
7
|
+
* Throws on missing/malformed frontmatter.
|
|
8
|
+
*/
|
|
9
|
+
export function parseFrontmatter(content) {
|
|
10
|
+
const m = /^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/.exec(content);
|
|
11
|
+
if (!m) {
|
|
12
|
+
throw new Error('missing or malformed frontmatter (--- delimiters)');
|
|
13
|
+
}
|
|
14
|
+
const raw = m[1];
|
|
15
|
+
const body = m[2];
|
|
16
|
+
|
|
17
|
+
const lines = raw.split(/\r?\n/);
|
|
18
|
+
let name = null;
|
|
19
|
+
let description = null;
|
|
20
|
+
|
|
21
|
+
for (let i = 0; i < lines.length; i++) {
|
|
22
|
+
const line = lines[i];
|
|
23
|
+
const nameMatch = /^name:\s*(.*)$/.exec(line);
|
|
24
|
+
if (nameMatch) {
|
|
25
|
+
name = nameMatch[1].trim();
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
const descMatch = /^description:\s*(.*)$/.exec(line);
|
|
29
|
+
if (descMatch) {
|
|
30
|
+
const rest = descMatch[1].trim();
|
|
31
|
+
if (rest === '>' || rest === '|' || rest === '') {
|
|
32
|
+
// Folded/literal block (`>` / `|`) OR multi-line plain scalar with
|
|
33
|
+
// value on continuation lines (prettier sometimes rewrote `>` blocks
|
|
34
|
+
// into this form). Either way, collect indented following lines.
|
|
35
|
+
const parts = [];
|
|
36
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
37
|
+
if (/^\s/.test(lines[j])) {
|
|
38
|
+
parts.push(lines[j].trim());
|
|
39
|
+
} else if (lines[j] === '') {
|
|
40
|
+
continue;
|
|
41
|
+
} else {
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
description = parts.filter(Boolean).join(' ');
|
|
46
|
+
} else {
|
|
47
|
+
description = rest;
|
|
48
|
+
}
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!name) {
|
|
54
|
+
throw new Error('frontmatter missing `name:`');
|
|
55
|
+
}
|
|
56
|
+
if (!description) {
|
|
57
|
+
throw new Error('frontmatter missing `description:`');
|
|
58
|
+
}
|
|
59
|
+
return { name, description, body };
|
|
60
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# Impulso opencode install — creates the isolated profile dir, symlinks
|
|
5
|
+
# skills/commands from this repo, writes opencode.jsonc + AGENTS.md.
|
|
6
|
+
# Idempotent — safe to rerun.
|
|
7
|
+
|
|
8
|
+
REPO="$(cd "$(dirname "$0")/../.." && pwd)"
|
|
9
|
+
PROFILE_DIR="${HOME}/.config/impulso-profile/opencode"
|
|
10
|
+
|
|
11
|
+
echo "==> Impulso install (opencode)"
|
|
12
|
+
echo " Repo: ${REPO}"
|
|
13
|
+
echo " Profile: ${PROFILE_DIR}"
|
|
14
|
+
|
|
15
|
+
# --- Preflight: hard-required binaries -------------------------------------
|
|
16
|
+
# Impulso is opinionated: tokensave (MCP server) and rtk (CLI token-filter
|
|
17
|
+
# proxy) must be on PATH. Missing either aborts install before touching
|
|
18
|
+
# the filesystem.
|
|
19
|
+
|
|
20
|
+
require_bin() {
|
|
21
|
+
local bin="$1"
|
|
22
|
+
local hint="$2"
|
|
23
|
+
if ! command -v "$bin" >/dev/null 2>&1; then
|
|
24
|
+
echo "ERROR: '$bin' not found on PATH. Impulso requires it." >&2
|
|
25
|
+
echo " $hint" >&2
|
|
26
|
+
exit 1
|
|
27
|
+
fi
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
require_bin rtk "Install: brew install rtk-ai/tap/rtk (see https://www.rtk-ai.app)"
|
|
31
|
+
require_bin tokensave "Install: brew install aovestdipaperino/tap/tokensave (or: cargo install tokensave)"
|
|
32
|
+
|
|
33
|
+
# --- Global gitignore for tool artifacts ------------------------------------
|
|
34
|
+
# .tokensave/ (per-project SQLite code graph) and .rtk/ (cache) must never
|
|
35
|
+
# land in a target repo. Write to a global git excludesfile so any repo
|
|
36
|
+
# the user touches with Impulso is protected. Prompted; default yes.
|
|
37
|
+
|
|
38
|
+
ensure_global_gitignore() {
|
|
39
|
+
local ignore_file="${HOME}/.config/git/ignore"
|
|
40
|
+
local entries=(".tokensave/" ".rtk/")
|
|
41
|
+
local answer="y"
|
|
42
|
+
|
|
43
|
+
echo
|
|
44
|
+
echo "Impulso can add .tokensave/ and .rtk/ to a global git excludesfile"
|
|
45
|
+
echo "so they are never committed to any target repo."
|
|
46
|
+
printf "Write to %s? [Y/n] " "${ignore_file}"
|
|
47
|
+
read -r answer || answer=""
|
|
48
|
+
|
|
49
|
+
# ${answer,,} is bash 4+; macOS ships bash 3.2 — use case instead.
|
|
50
|
+
case "${answer}" in
|
|
51
|
+
'' | [Yy]) ;;
|
|
52
|
+
*)
|
|
53
|
+
echo " Skipped. Add these lines manually if you change your mind:"
|
|
54
|
+
printf " %s\n" "${entries[@]}"
|
|
55
|
+
echo " then: git config --global core.excludesfile <path>"
|
|
56
|
+
return 0
|
|
57
|
+
;;
|
|
58
|
+
esac
|
|
59
|
+
|
|
60
|
+
mkdir -p "$(dirname "${ignore_file}")"
|
|
61
|
+
touch "${ignore_file}"
|
|
62
|
+
|
|
63
|
+
local entry
|
|
64
|
+
for entry in "${entries[@]}"; do
|
|
65
|
+
if ! grep -Fxq -- "${entry}" "${ignore_file}" 2>/dev/null; then
|
|
66
|
+
printf '%s\n' "${entry}" >> "${ignore_file}"
|
|
67
|
+
echo " Added: ${entry}"
|
|
68
|
+
else
|
|
69
|
+
echo " Already present: ${entry}"
|
|
70
|
+
fi
|
|
71
|
+
done
|
|
72
|
+
|
|
73
|
+
git config --global core.excludesfile "${ignore_file}"
|
|
74
|
+
echo " core.excludesfile = ${ignore_file}"
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
ensure_global_gitignore
|
|
78
|
+
|
|
79
|
+
# --- Profile setup ----------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
mkdir -p "${PROFILE_DIR}"
|
|
82
|
+
|
|
83
|
+
# Symlink skills/ and commands/ into the profile. Use -sfn (force, no-deref
|
|
84
|
+
# target) so we don't follow into the target when replacing.
|
|
85
|
+
for sub in skills commands; do
|
|
86
|
+
link_path="${PROFILE_DIR}/${sub}"
|
|
87
|
+
target="${REPO}/${sub}"
|
|
88
|
+
if [[ -L "${link_path}" ]] || [[ -e "${link_path}" ]]; then
|
|
89
|
+
rm -rf "${link_path}"
|
|
90
|
+
fi
|
|
91
|
+
ln -sfn "${target}" "${link_path}"
|
|
92
|
+
echo " Linked ${sub} -> ${target}"
|
|
93
|
+
done
|
|
94
|
+
|
|
95
|
+
# Copy AGENTS.md from the canonical shared/ source (copy, not symlink —
|
|
96
|
+
# profile self-contained).
|
|
97
|
+
cp -f "${REPO}/shared/AGENTS.md" "${PROFILE_DIR}/AGENTS.md"
|
|
98
|
+
echo " Copied AGENTS.md"
|
|
99
|
+
|
|
100
|
+
# Generate opencode.jsonc from the self-boot config (single source of truth):
|
|
101
|
+
# the self-boot file resolves plugins relative to the config dir ("../...");
|
|
102
|
+
# the profile pins them to this repo's absolute path instead.
|
|
103
|
+
sed "s|\"\.\./|\"${REPO}/|g" "${REPO}/opencode/opencode.jsonc" > "${PROFILE_DIR}/opencode.jsonc"
|
|
104
|
+
echo " Wrote opencode.jsonc (plugins -> ${REPO}/harnesses/opencode)"
|
|
105
|
+
|
|
106
|
+
# --- impulso-hub binary — copy matching bundled package binary -------------
|
|
107
|
+
node "${REPO}/scripts/install-hub.js"
|
|
108
|
+
|
|
109
|
+
if ! command -v impulso-hub >/dev/null 2>&1; then
|
|
110
|
+
echo " NOTE: ~/.local/bin may not be on PATH. Add to your shell profile:"
|
|
111
|
+
echo ' export PATH="$HOME/.local/bin:$PATH"'
|
|
112
|
+
fi
|
|
113
|
+
|
|
114
|
+
# Remove old impulso-gate binary if present (renamed to impulso-hub)
|
|
115
|
+
OLD_GATE="${HOME}/.local/bin/impulso-gate"
|
|
116
|
+
if [ -f "${OLD_GATE}" ] || [ -L "${OLD_GATE}" ]; then
|
|
117
|
+
rm -f "${OLD_GATE}" && echo " Removed old impulso-gate binary"
|
|
118
|
+
fi
|
|
119
|
+
|
|
120
|
+
echo "==> Done. Launch with:"
|
|
121
|
+
echo " XDG_CONFIG_HOME=${HOME}/.config/impulso-profile opencode"
|