gipity 1.0.381 → 1.0.384
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/commands/add.js +6 -1
- package/dist/commands/claude.js +2 -2
- package/dist/commands/status.js +28 -34
- package/dist/commands/sync.js +1 -1
- package/dist/commands/test.js +53 -7
- package/dist/commands/uninstall.js +46 -4
- package/dist/hooks/capture-runner.js +4 -2
- package/dist/knowledge.js +14 -3
- package/dist/prompts.js +0 -7
- package/dist/relay/daemon.js +74 -26
- package/dist/relay/redact.js +11 -3
- package/dist/relay/state.js +19 -1
- package/dist/setup.js +112 -108
- package/dist/sync.js +35 -8
- package/package.json +1 -1
package/dist/commands/add.js
CHANGED
|
@@ -9,8 +9,10 @@ import { success, muted, bold } from '../colors.js';
|
|
|
9
9
|
import { run } from '../helpers/index.js';
|
|
10
10
|
const STARTERS = [
|
|
11
11
|
{ key: 'web-vision-cam', hint: 'fullscreen camera app with on-device vision (MediaPipe)' },
|
|
12
|
+
{ key: 'object-spotter', hint: 'camera app that boxes, labels, and counts objects (YOLOX on-device)' },
|
|
12
13
|
{ key: '2d-game', hint: '2D games with Phaser 3 - platformer, arcade, puzzle' },
|
|
13
14
|
{ key: '3d-world', hint: 'playable 3D multiplayer rocket-launcher demo' },
|
|
15
|
+
{ key: 'karaoke-captions', hint: 'audio + lyrics -> word-synced karaoke captions (GPU job)' },
|
|
14
16
|
];
|
|
15
17
|
const BLANK = [
|
|
16
18
|
{ key: 'web-simple', hint: 'static frontend-only site - pages, dashboards, simple games' },
|
|
@@ -22,6 +24,9 @@ const HIDDEN = [{ key: 'app-itsm', hint: 'IT service management / helpdesk / tic
|
|
|
22
24
|
const KITS = [
|
|
23
25
|
{ key: 'realtime', hint: 'multiplayer / presence / shared state' },
|
|
24
26
|
{ key: 'web-vision-mediapipe', hint: 'browser camera vision - gesture, pose, object detection' },
|
|
27
|
+
{ key: 'web-vision-detect', hint: 'browser object detection - YOLOX, WebGPU/WASM, custom models' },
|
|
28
|
+
{ key: 'chatbot', hint: 'drop-in chatbot - persona, guardrails, streaming responses' },
|
|
29
|
+
{ key: 'audio-align', hint: 'audio + lyrics -> word-level timing JSON (GPU job)' },
|
|
25
30
|
{ key: 'i18n', hint: 'multi-language web apps - language picker, RTL, translations' },
|
|
26
31
|
];
|
|
27
32
|
// The catalog block, rendered once and reused by the full help output
|
|
@@ -142,7 +147,7 @@ export const addCommand = new Command('add')
|
|
|
142
147
|
.argument('[name]', 'Template/kit key, OR a local directory path (./, ~/, or /abs). Omit for help; use --list for just the catalog.')
|
|
143
148
|
.option('--title <title>', 'App title - templates only (defaults to project name)')
|
|
144
149
|
.option('--description <desc>', 'App description for meta tags - templates only')
|
|
145
|
-
.option('--force', 'Templates only:
|
|
150
|
+
.option('--force', 'Templates only: install into a non-empty project (same-named files get a new version; other files are kept)')
|
|
146
151
|
.option('--list', 'List the template/kit catalog and exit')
|
|
147
152
|
.option('--json', 'Output as JSON')
|
|
148
153
|
.addHelpText('after', () => catalogText() + '\n\n'
|
package/dist/commands/claude.js
CHANGED
|
@@ -676,8 +676,8 @@ export const claudeCommand = new Command('claude')
|
|
|
676
676
|
}
|
|
677
677
|
if (!nonInteractive) {
|
|
678
678
|
console.log(` ${bold('Launching Claude Code, powered by Gipity.')}`);
|
|
679
|
-
console.log(` ${muted("Just tell Claude what you'd like to build or do - everything Claude
|
|
680
|
-
console.log(` ${muted('
|
|
679
|
+
console.log(` ${muted("Just tell Claude what you'd like to build or do - everything Claude can do,")}`);
|
|
680
|
+
console.log(` ${muted('plus hosting, databases, and live deploys on Gipity.')}`);
|
|
681
681
|
console.log('');
|
|
682
682
|
}
|
|
683
683
|
// In non-interactive (-p) mode, prepend a Gipity preamble to the
|
package/dist/commands/status.js
CHANGED
|
@@ -1,48 +1,40 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import { existsSync, readFileSync } from 'fs';
|
|
3
3
|
import { join, resolve } from 'path';
|
|
4
|
+
import { homedir } from 'os';
|
|
4
5
|
import { getAuth, getTimeRemaining } from '../auth.js';
|
|
5
6
|
import { getConfig, liveUrl } from '../config.js';
|
|
6
7
|
import { brand, success, warning, muted, error as clrError } from '../colors.js';
|
|
7
|
-
import {
|
|
8
|
-
/**
|
|
9
|
-
*
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
}
|
|
18
|
-
catch {
|
|
19
|
-
return { missing: Object.keys(HOOKS_SETTINGS.hooks), ok: false };
|
|
20
|
-
}
|
|
21
|
-
const actualHooks = settings?.hooks ?? {};
|
|
22
|
-
const missing = [];
|
|
23
|
-
for (const [event, expectedGroups] of Object.entries(HOOKS_SETTINGS.hooks)) {
|
|
24
|
-
const actualGroups = Array.isArray(actualHooks[event]) ? actualHooks[event] : [];
|
|
25
|
-
const expectedCmds = new Set(expectedGroups.flatMap(g => (g.hooks ?? []).map((h) => h.command)));
|
|
26
|
-
const actualCmds = new Set(actualGroups.flatMap(g => (g.hooks ?? []).map((h) => h.command)));
|
|
27
|
-
// Every expected command must be present. Users can add their own.
|
|
28
|
-
for (const cmd of expectedCmds) {
|
|
29
|
-
if (!actualCmds.has(cmd)) {
|
|
30
|
-
missing.push(event);
|
|
31
|
-
break;
|
|
32
|
-
}
|
|
8
|
+
import { GIPITY_PLUGIN_ID, GIPITY_MARKETPLACE_NAME, setupClaudeHooks, ensureGipityPlugin } from '../setup.js';
|
|
9
|
+
/** Hooks ship in the Gipity Claude Code plugin now - "installed" means the
|
|
10
|
+
* user-scope settings register the marketplace and enable the plugin.
|
|
11
|
+
* Claude Code fetches/updates the plugin itself at launch. */
|
|
12
|
+
function checkGipityPlugin() {
|
|
13
|
+
const path = join(homedir(), '.claude', 'settings.json');
|
|
14
|
+
let settings = {};
|
|
15
|
+
if (existsSync(path)) {
|
|
16
|
+
try {
|
|
17
|
+
settings = JSON.parse(readFileSync(path, 'utf-8'));
|
|
33
18
|
}
|
|
19
|
+
catch { /* treat as empty */ }
|
|
34
20
|
}
|
|
21
|
+
const missing = [];
|
|
22
|
+
if (!settings?.extraKnownMarketplaces?.[GIPITY_MARKETPLACE_NAME])
|
|
23
|
+
missing.push('marketplace');
|
|
24
|
+
if (settings?.enabledPlugins?.[GIPITY_PLUGIN_ID] !== true)
|
|
25
|
+
missing.push('plugin');
|
|
35
26
|
return { missing, ok: missing.length === 0 };
|
|
36
27
|
}
|
|
37
28
|
export const statusCommand = new Command('status')
|
|
38
29
|
.description('Show project and login status')
|
|
39
30
|
.option('--json', 'Output as JSON')
|
|
40
|
-
.option('--repair-hooks', '
|
|
31
|
+
.option('--repair-hooks', 'Re-enable the Gipity Claude Code plugin (hooks) if missing or disabled')
|
|
41
32
|
.action(async (opts) => {
|
|
42
33
|
const config = getConfig();
|
|
43
34
|
const auth = getAuth();
|
|
44
35
|
const cwd = resolve(process.cwd());
|
|
45
|
-
|
|
36
|
+
void cwd;
|
|
37
|
+
const hookCheck = config ? checkGipityPlugin() : null;
|
|
46
38
|
if (opts.json) {
|
|
47
39
|
console.log(JSON.stringify({
|
|
48
40
|
project: config ? {
|
|
@@ -57,7 +49,7 @@ export const statusCommand = new Command('status')
|
|
|
57
49
|
expiresAt: auth.expiresAt,
|
|
58
50
|
valid: new Date(auth.expiresAt).getTime() > Date.now(),
|
|
59
51
|
} : null,
|
|
60
|
-
|
|
52
|
+
plugin: hookCheck,
|
|
61
53
|
}, null, 2));
|
|
62
54
|
return;
|
|
63
55
|
}
|
|
@@ -80,16 +72,18 @@ export const statusCommand = new Command('status')
|
|
|
80
72
|
}
|
|
81
73
|
if (hookCheck) {
|
|
82
74
|
if (hookCheck.ok) {
|
|
83
|
-
console.log(`${muted('Hooks:')} ${success(
|
|
75
|
+
console.log(`${muted('Hooks:')} ${success(`Gipity plugin enabled (${GIPITY_PLUGIN_ID})`)}`);
|
|
84
76
|
}
|
|
85
77
|
else if (opts.repairHooks) {
|
|
78
|
+
// force: an explicit repair request overrides a previous disable.
|
|
79
|
+
ensureGipityPlugin(true);
|
|
86
80
|
setupClaudeHooks();
|
|
87
|
-
console.log(`${muted('Hooks:')} ${success('repaired - re-
|
|
81
|
+
console.log(`${muted('Hooks:')} ${success('repaired - Gipity plugin re-enabled')}`);
|
|
88
82
|
}
|
|
89
83
|
else {
|
|
90
|
-
console.log(`${muted('Hooks:')} ${warning(`missing
|
|
91
|
-
console.log(muted('Run `gipity status --repair-hooks` to re-
|
|
92
|
-
console.log(muted('Without
|
|
84
|
+
console.log(`${muted('Hooks:')} ${warning(`Gipity plugin not enabled (missing: ${hookCheck.missing.join(', ')})`)}`);
|
|
85
|
+
console.log(muted('Run `gipity status --repair-hooks` to re-enable.'));
|
|
86
|
+
console.log(muted('Without it, files don\'t auto-sync and web CLI dispatches can\'t show Claude Code output.'));
|
|
93
87
|
}
|
|
94
88
|
}
|
|
95
89
|
});
|
package/dist/commands/sync.js
CHANGED
|
@@ -3,7 +3,7 @@ import { sync } from '../sync.js';
|
|
|
3
3
|
import { createProgressReporter } from '../progress.js';
|
|
4
4
|
import { error as clrError } from '../colors.js';
|
|
5
5
|
export const syncCommand = new Command('sync')
|
|
6
|
-
.description('Sync files')
|
|
6
|
+
.description('Sync files (a .gipityignore at the project root excludes paths, gitignore-style)')
|
|
7
7
|
.option('--plan', 'Print the plan without applying any changes')
|
|
8
8
|
.option('--force', 'Bypass the bulk-deletion guard')
|
|
9
9
|
.option('--json', 'Output as JSON')
|
package/dist/commands/test.js
CHANGED
|
@@ -12,6 +12,8 @@ function statusIcon(status) {
|
|
|
12
12
|
return muted('→');
|
|
13
13
|
return muted('?');
|
|
14
14
|
}
|
|
15
|
+
// Long-run hint for non-TTY runs: after this, print a one-time "not hung" + faster-path hint.
|
|
16
|
+
const LONG_RUN_MS = 60000;
|
|
15
17
|
async function pollTestStatus(projectGuid, runGuid, opts) {
|
|
16
18
|
// Adaptive polling: fast at first (tests often finish quickly with warm pool),
|
|
17
19
|
// then back off for long-running suites.
|
|
@@ -24,7 +26,21 @@ async function pollTestStatus(projectGuid, runGuid, opts) {
|
|
|
24
26
|
return 1000; // next 15s: poll every 1s
|
|
25
27
|
return 3000; // after 20s: poll every 3s
|
|
26
28
|
};
|
|
29
|
+
// Heartbeat cadence for non-TTY runs (background/piped output, e.g. a watching
|
|
30
|
+
// agent): every line is re-read by the watcher, and on long LLM-backed suites
|
|
31
|
+
// nothing changes second-to-second — so back off like getPollInterval does.
|
|
32
|
+
const getHeartbeatInterval = () => {
|
|
33
|
+
const elapsed = Date.now() - startTime;
|
|
34
|
+
if (elapsed < 120000)
|
|
35
|
+
return 10000; // first 2 min: every 10s
|
|
36
|
+
if (elapsed < 300000)
|
|
37
|
+
return 30000; // to 5 min: every 30s
|
|
38
|
+
return 60000; // after 5 min: every 60s
|
|
39
|
+
};
|
|
27
40
|
let lastResultCount = 0;
|
|
41
|
+
const isTTY = !!process.stdout.isTTY;
|
|
42
|
+
let lastHeartbeat = 0; // 0 => emit the first heartbeat immediately (non-TTY)
|
|
43
|
+
let longRunHintShown = false;
|
|
28
44
|
while (true) {
|
|
29
45
|
const res = await get(`/projects/${projectGuid}/test/status/${runGuid}`);
|
|
30
46
|
const data = res.data;
|
|
@@ -61,14 +77,38 @@ async function pollTestStatus(projectGuid, runGuid, opts) {
|
|
|
61
77
|
if (data.status !== 'running') {
|
|
62
78
|
return data;
|
|
63
79
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
80
|
+
if (!opts.json) {
|
|
81
|
+
if (isTTY) {
|
|
82
|
+
// Real terminal: overwrite a single in-place progress line.
|
|
83
|
+
if (data.totalFiles === 0) {
|
|
84
|
+
process.stdout.write(`\r ${muted('Starting...')} `);
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
const pct = Math.round((data.completedFiles / data.totalFiles) * 100);
|
|
88
|
+
process.stdout.write(`\r ${muted(`${data.completedFiles}/${data.totalFiles} files (${pct}%)`)}`);
|
|
89
|
+
}
|
|
68
90
|
}
|
|
69
91
|
else {
|
|
70
|
-
|
|
71
|
-
|
|
92
|
+
// Non-TTY (background/piped, e.g. a watching agent): a \r line never
|
|
93
|
+
// shows up in a captured file, so emit periodic newline heartbeats.
|
|
94
|
+
// Time-based so the file always grows — lets a watcher tell a slow run
|
|
95
|
+
// from a hung one, and gives elapsed time to budget against.
|
|
96
|
+
const now = Date.now();
|
|
97
|
+
if (now - lastHeartbeat >= getHeartbeatInterval()) {
|
|
98
|
+
lastHeartbeat = now;
|
|
99
|
+
const elapsed = Math.round((now - startTime) / 1000);
|
|
100
|
+
const progress = data.totalFiles === 0
|
|
101
|
+
? 'starting up'
|
|
102
|
+
: `${data.completedFiles}/${data.totalFiles} files`;
|
|
103
|
+
const tally = data.passed + data.failed > 0
|
|
104
|
+
? ` (${data.passed} passed${data.failed > 0 ? `, ${data.failed} failed` : ''})`
|
|
105
|
+
: '';
|
|
106
|
+
console.log(muted(` … still running — ${progress}${tally}, ${elapsed}s elapsed`));
|
|
107
|
+
if (now - startTime >= LONG_RUN_MS && !longRunHintShown) {
|
|
108
|
+
longRunHintShown = true;
|
|
109
|
+
console.log(muted(' Note: progressing, not hung. LLM-backed tests can take minutes. To verify one function fast, use `gipity fn call <name>`; narrow this suite with `gipity test <path>`.'));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
72
112
|
}
|
|
73
113
|
}
|
|
74
114
|
await new Promise(resolve => setTimeout(resolve, getPollInterval()));
|
|
@@ -78,13 +118,15 @@ async function pollTestStatus(projectGuid, runGuid, opts) {
|
|
|
78
118
|
export const testCommand = new Command('test')
|
|
79
119
|
.description('Run tests')
|
|
80
120
|
.argument('[path]', 'Test path filter (e.g. "api", "e2e/portal")')
|
|
121
|
+
.option('--filter <path>', 'Alias for the positional test path filter')
|
|
81
122
|
.option('--timeout <ms>', 'Per-test timeout in ms', '30000')
|
|
82
123
|
.option('--retry <n>', 'Retry failed tests N times', '0')
|
|
83
124
|
.option('--no-sync', 'Skip sync-up before tests')
|
|
84
125
|
.option('--json', 'Output as JSON')
|
|
85
|
-
.action((
|
|
126
|
+
.action((pathFilter, opts) => run('Test', async () => {
|
|
86
127
|
const config = requireConfig();
|
|
87
128
|
await syncBeforeAction(opts);
|
|
129
|
+
const filterPath = opts.filter || pathFilter;
|
|
88
130
|
if (!opts.json) {
|
|
89
131
|
console.log(bold(`Running tests: ${filterPath || 'all'}`));
|
|
90
132
|
console.log('');
|
|
@@ -113,6 +155,10 @@ export const testCommand = new Command('test')
|
|
|
113
155
|
// Print any remaining results not yet shown (edge case: final batch)
|
|
114
156
|
// (pollTestStatus already printed results incrementally)
|
|
115
157
|
console.log('');
|
|
158
|
+
if (filterPath && data.total === 0 && data.results.length === 0) {
|
|
159
|
+
console.log(clrError(`No tests matched filter: ${filterPath}`));
|
|
160
|
+
process.exit(1);
|
|
161
|
+
}
|
|
116
162
|
// Summary
|
|
117
163
|
const parts = [];
|
|
118
164
|
if (data.passed > 0)
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `gipity uninstall` - true reset. Stops the relay daemon, removes the
|
|
3
3
|
* platform autostart service, revokes the device on the server (best-effort),
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* removes the Gipity Claude Code plugin enablement (which removes all Gipity
|
|
5
|
+
* hooks at once), and wipes ~/.gipity/. Never touches ~/GipityProjects/ -
|
|
6
|
+
* your local project trees are yours to keep or remove yourself.
|
|
6
7
|
*
|
|
7
8
|
* Does not touch the npm-installed shim - the user removes that separately
|
|
8
9
|
* via `npm uninstall -g gipity`.
|
|
9
10
|
*/
|
|
10
11
|
import { Command } from 'commander';
|
|
11
|
-
import { existsSync, rmSync, unlinkSync } from 'fs';
|
|
12
|
+
import { existsSync, rmSync, unlinkSync, readFileSync, writeFileSync } from 'fs';
|
|
12
13
|
import { homedir } from 'os';
|
|
13
14
|
import { join, resolve } from 'path';
|
|
14
15
|
import { spawnSync } from 'child_process';
|
|
@@ -18,6 +19,39 @@ import { confirm, getAutoConfirm } from '../utils.js';
|
|
|
18
19
|
import { bold, brand, dim, success, error as clrError, muted } from '../colors.js';
|
|
19
20
|
import * as relayState from '../relay/state.js';
|
|
20
21
|
import { planFor, UnsupportedPlatformError } from '../relay/installers.js';
|
|
22
|
+
import { GIPITY_PLUGIN_ID, GIPITY_MARKETPLACE_NAME, stripGipityHooks } from '../setup.js';
|
|
23
|
+
/** Remove Gipity's entries from the user-scope Claude Code settings: the
|
|
24
|
+
* plugin enablement, the marketplace registration, and any legacy hook
|
|
25
|
+
* blocks older CLI versions wrote there. Surgical - everything else in the
|
|
26
|
+
* file (the user's own permissions, hooks, other plugins) is untouched. */
|
|
27
|
+
function removeGipityPluginConfig() {
|
|
28
|
+
const settingsPath = join(homedir(), '.claude', 'settings.json');
|
|
29
|
+
if (!existsSync(settingsPath))
|
|
30
|
+
return false;
|
|
31
|
+
let settings;
|
|
32
|
+
try {
|
|
33
|
+
settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
let changed = stripGipityHooks(settings);
|
|
39
|
+
if (settings.enabledPlugins && GIPITY_PLUGIN_ID in settings.enabledPlugins) {
|
|
40
|
+
delete settings.enabledPlugins[GIPITY_PLUGIN_ID];
|
|
41
|
+
if (Object.keys(settings.enabledPlugins).length === 0)
|
|
42
|
+
delete settings.enabledPlugins;
|
|
43
|
+
changed = true;
|
|
44
|
+
}
|
|
45
|
+
if (settings.extraKnownMarketplaces?.[GIPITY_MARKETPLACE_NAME]) {
|
|
46
|
+
delete settings.extraKnownMarketplaces[GIPITY_MARKETPLACE_NAME];
|
|
47
|
+
if (Object.keys(settings.extraKnownMarketplaces).length === 0)
|
|
48
|
+
delete settings.extraKnownMarketplaces;
|
|
49
|
+
changed = true;
|
|
50
|
+
}
|
|
51
|
+
if (changed)
|
|
52
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
53
|
+
return changed;
|
|
54
|
+
}
|
|
21
55
|
function resolveCliPath() {
|
|
22
56
|
return resolve(process.argv[1] ?? 'gipity');
|
|
23
57
|
}
|
|
@@ -99,6 +133,7 @@ export const uninstallCommand = new Command('uninstall')
|
|
|
99
133
|
console.log(`• Stop the running relay daemon (if any)`);
|
|
100
134
|
console.log(`• Remove the OS autostart service (launchd / systemd / Task Scheduler)`);
|
|
101
135
|
console.log(`• Revoke this device on the server (best-effort)`);
|
|
136
|
+
console.log(`• Remove the Gipity Claude Code plugin enablement (all Gipity hooks)`);
|
|
102
137
|
console.log(`• Delete ${gipityDir}/`);
|
|
103
138
|
console.log('');
|
|
104
139
|
console.log(`${dim('It will NOT remove the `gipity` binary. Run `npm uninstall -g gipity` afterward if you want that too.')}`);
|
|
@@ -124,7 +159,14 @@ export const uninstallCommand = new Command('uninstall')
|
|
|
124
159
|
// 3. Revoke device on server.
|
|
125
160
|
await revokeDeviceBestEffort();
|
|
126
161
|
console.log(`${success('Device revoked on server (or was already revoked).')}`);
|
|
127
|
-
// 4.
|
|
162
|
+
// 4. Remove the Claude Code plugin enablement + any legacy hook blocks.
|
|
163
|
+
if (removeGipityPluginConfig()) {
|
|
164
|
+
console.log(`${success('Gipity Claude Code plugin disabled (hooks removed).')}`);
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
console.log(`${muted('No Gipity entries in Claude Code settings.')}`);
|
|
168
|
+
}
|
|
169
|
+
// 5. Wipe ~/.gipity/.
|
|
128
170
|
if (existsSync(gipityDir)) {
|
|
129
171
|
try {
|
|
130
172
|
rmSync(gipityDir, { recursive: true, force: true });
|
|
@@ -6,8 +6,10 @@
|
|
|
6
6
|
* session into the Gipity server so the web CLI can display it read-only.
|
|
7
7
|
*
|
|
8
8
|
* Not a user-facing `gipity` subcommand by design: users never invoke
|
|
9
|
-
* this directly.
|
|
10
|
-
*
|
|
9
|
+
* this directly. The Gipity Claude Code plugin's capture hook script
|
|
10
|
+
* (claude-plugin hooks/scripts/capture.cjs) resolves this file inside the
|
|
11
|
+
* installed CLI at fire time and runs it - so the capture logic versions
|
|
12
|
+
* with the CLI, not the plugin.
|
|
11
13
|
*
|
|
12
14
|
* Usage:
|
|
13
15
|
* node capture-runner.js <source> <event>
|
package/dist/knowledge.js
CHANGED
|
@@ -14,6 +14,7 @@ Templates:
|
|
|
14
14
|
- \`web-simple\` - Landing page, dashboard, calculator, canvas demo, visualization, animation, single-page tool
|
|
15
15
|
- \`web-fullstack\` - Web app with login, database, or API - CRM, invoice tracker, booking system, admin panel
|
|
16
16
|
- \`web-vision-cam\` - Live camera app with gesture, pose, or object detection - hand-tracking input, fitness/pose feedback, object-aware UI; on-device, no upload
|
|
17
|
+
- \`object-spotter\` - Object-detection app - count or label things from the camera or a photo, inventory/shelf counting, custom-model detection; on-device, no upload
|
|
17
18
|
- \`2d-game\` - Platformer, arcade, puzzle, endless runner, physics toy (Phaser 3)
|
|
18
19
|
- \`3d-engine\` - Minimal 3D multiplayer base - Three.js + Rapier + Colyseus, no gameplay; build your own scene and mechanics on top
|
|
19
20
|
- \`3d-world\` - Multiplayer world, 3D sandbox, shooter, exploration, virtual showroom (Three.js + Rapier + Colyseus)
|
|
@@ -29,9 +30,13 @@ Hidden types (do NOT suggest unsolicited - use only when the user explicitly ask
|
|
|
29
30
|
Kits are reusable building blocks added to an existing app, not whole templates - their files land in \`src/packages/<name>/\`:
|
|
30
31
|
- \`gipity add realtime\` - Multiplayer / presence / shared state - channels, host election, server-persisted sync. Engine-agnostic; works in any app.
|
|
31
32
|
- \`gipity add web-vision-mediapipe\` - On-device camera vision - gesture recognition, body pose, object detection. Runs fully client-side via MediaPipe Tasks; no server, no upload. Web only.
|
|
33
|
+
- \`gipity add web-vision-detect\` - High-accuracy object detection in the browser - YOLOX (Apache-2.0) on ONNX Runtime Web, WebGPU with WASM fallback. 80 COCO classes, three speed/accuracy presets, or bring your own custom-trained YOLO/YOLOX ONNX model. Client-side; no server, no upload. Web only.
|
|
32
34
|
- \`gipity add chatbot\` - Drop-in chatbot - configurable persona, scope guardrails, static knowledge (20k budget), streaming responses. Headless engine + bubble widget; bring your own UI if you want. Works in any app.
|
|
33
35
|
- \`gipity add audio-align\` - Audio + lyrics -> word-level timing JSON. Demucs vocal isolation + MMS_FA forced alignment, runs as a Modal L4 GPU job (~$0.01 per 3-min song). For karaoke captions, subtitling, language learning, dubbing alignment.
|
|
34
|
-
- \`gipity add i18n\` - Multi-language for web apps - language picker, locale persistence, RTL, plural/translation lookup. Scaffolds src/js/strings.js and wires it up; move your copy there and read it with t('key'). Web only
|
|
36
|
+
- \`gipity add i18n\` - Multi-language for web apps - language picker, locale persistence, RTL, plural/translation lookup. Scaffolds src/js/strings.js and wires it up; move your copy there and read it with t('key'). Web only.
|
|
37
|
+
- \`gipity add records\` - Registry-driven records: declare objects/fields as data, get generic CRUD functions with validation, full-text search, soft delete, ACTOR provenance, and an audit event spine - every write is transactional (row + event). Field types include relations ({id,label}), currency, emails/phones/links composites. Ships backend functions + migrations. Needs a database (web-fullstack/api template).
|
|
38
|
+
- \`gipity add views\` - Generic UI over records-kit objects: sortable/filterable table with full-text search, create/edit/delete forms with type-appropriate widgets, kanban board with drag-to-update. Renders entirely from the field registry - zero per-object UI code. Requires the records kit.
|
|
39
|
+
- \`gipity add agent-api\` - Make your app agent-operable: named API keys (kit_api_keys) let agents and scripts write through the records kit's single write path with AGENT/API actor attribution - machine writes land on the same audit spine as human edits. Requires the records kit.`;
|
|
35
40
|
export const SKILLS_CONTENT = `# Gipity Integration
|
|
36
41
|
|
|
37
42
|
Gipity is the cloud platform your project runs on - hosting, databases, deployment, file storage, code execution, workflows, and monitoring. Gip is the cloud agent that runs on Gipity.
|
|
@@ -43,12 +48,16 @@ Prefer the cheapest option that works - CLI and sandbox are instant and free, ap
|
|
|
43
48
|
3. App services - runtime HTTP endpoints your deployed app calls directly at \`https://a.gipity.ai/api/<PROJECT_GUID>/services/*\`. Available: LLM, TTS, image, sound, music, transcribe, video, file upload, realtime, location. Load the matching skill (\`app-llm\`, \`app-tts\`, etc.) before writing service code - they have the schemas, auth pattern, and common-mistake guards. For one-off generation during development, prefer \`gipity generate <image|video|speech|music>\` or \`gipity chat\`. \`gipity generate\` saves to a generic file in the current directory by default (e.g. \`./generated.png\`) — pass \`-o <path>\` to write it straight into your source tree so it deploys (e.g. \`gipity generate image "hero banner" -o src/assets/images/hero.png\`) instead of generating at cwd and moving it.
|
|
44
49
|
4. Delegate to Gip (\`gipity chat "<task>"\`) - only when the work genuinely needs agent reasoning or a tool not in the CLI, sandbox, or app services. Required for: Twitter/X search, Gmail, calendar, push notifications, video understanding, audio source isolation, cross-model second opinions, multi-step orchestration. Don't use \`gipity chat\` for anything the sandbox can do - it's slower and burns tokens.
|
|
45
50
|
|
|
46
|
-
You are the developer. Write files in this directory -
|
|
51
|
+
You are the developer. Write files in this directory - the Gipity Claude Code plugin's hooks auto-sync them to Gipity. Don't run \`npm install\`, \`npm start\`, \`node\`, or \`python\` locally; there is no local runtime. Code runs in the Gipity sandbox.
|
|
47
52
|
|
|
48
53
|
## Use first-party services before reaching outside
|
|
49
54
|
|
|
50
55
|
Gipity ships first-party services for what apps usually pull from third parties - auth, location/geocoding, LLM, image/audio/video generation, transcription, file uploads, realtime. Before calling an external API or adding an npm package for one of these, check \`gipity skill list\` for a match. First-party services need no API keys, cost less, and keep data in-house. Reach outside only when the catalog has no equivalent - and say so when you do.
|
|
51
56
|
|
|
57
|
+
## Don't guess Gipity facts - look them up
|
|
58
|
+
|
|
59
|
+
When a user asks about Gipity itself - how to install it, what it costs, what's shipped, how a command or flag works - answer from an authoritative source, not memory, and don't hedge with "I don't want to guess." Check in this order: (1) \`gipity skill read <name>\` / \`gipity skill list\` - install and account basics live in \`getting-started\`; (2) \`gipity <command> --help\` for command syntax; (3) if it's genuinely not in the skills or CLI help, fetch the live site at \`https://gipity.ai\` (or web search) and cite what you found. A wrong or vague answer about the product is worse than spending one tool call to get the current, correct one.
|
|
60
|
+
|
|
52
61
|
## Gipity is opinionated - build on its stack
|
|
53
62
|
|
|
54
63
|
Gipity is an opinionated platform with its own best-practice stack, and that stack is the one you use - whatever tools the user names. The platform layer is fixed:
|
|
@@ -84,7 +93,9 @@ Every tool call returns its full output with that call. There is no output buffe
|
|
|
84
93
|
|
|
85
94
|
## Files and sync
|
|
86
95
|
|
|
87
|
-
Write files locally - hooks auto-push to Gipity
|
|
96
|
+
Write files locally - the Gipity Claude Code plugin's hooks auto-push every save to Gipity and auto-pull remote changes (images, audio from \`gipity chat\`) before each turn. Use \`gipity sync\` if things get out of sync (or if the plugin isn't installed - \`gipity status --repair-hooks\` re-enables it). Deletes are safe - use \`rollback\` with a datetime to undo, or \`file_version_restore\` for individual files.
|
|
97
|
+
|
|
98
|
+
To keep local-only material (research clones, scratch data, vendored references) in the project directory without syncing or deploying it, list it in a \`.gipityignore\` at the project root - gitignore-style, one pattern per line, \`#\` comments. Ignored paths are invisible to sync in both directions; anything that already synced before being ignored stays on the server until you delete it.
|
|
88
99
|
|
|
89
100
|
## Skills (detailed documentation)
|
|
90
101
|
|
package/dist/prompts.js
CHANGED
|
@@ -162,13 +162,6 @@ export function buildFreshWrap(contextBlock, userMsg) {
|
|
|
162
162
|
].join('\n');
|
|
163
163
|
}
|
|
164
164
|
// ---------------------------------------------------------------------------
|
|
165
|
-
// PreToolUse soft-warning hook (advisory message printed to stderr)
|
|
166
|
-
// ---------------------------------------------------------------------------
|
|
167
|
-
/** Plain ASCII, no apostrophes or backslashes - embedded inside a node -e shell command. */
|
|
168
|
-
export const SCAFFOLD_HOOK_WARNING = `[gipity] Heads up: this project has no app yet. If you are building an app/game/API to deploy, ` +
|
|
169
|
-
`stop and run: gipity add <${TEMPLATE_KEY_PATTERN}> (default: web-simple). ` +
|
|
170
|
-
`If this is a one-off task (analysis, data, PDFs, scratch work), proceed.`;
|
|
171
|
-
// ---------------------------------------------------------------------------
|
|
172
165
|
// Per-project CLAUDE.md / AGENTS.md body.
|
|
173
166
|
//
|
|
174
167
|
// The content (`SKILLS_CONTENT`) is sourced from
|
package/dist/relay/daemon.js
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* See docs/feature-backlog/gipity-relay-phases.md (Phase A Step 7).
|
|
22
22
|
*/
|
|
23
23
|
import { spawn } from 'child_process';
|
|
24
|
-
import { appendFileSync, mkdirSync, existsSync, readFileSync, writeFileSync, chmodSync, closeSync, openSync } from 'fs';
|
|
24
|
+
import { appendFileSync, mkdirSync, existsSync, readFileSync, writeFileSync, chmodSync, closeSync, openSync, unlinkSync } from 'fs';
|
|
25
25
|
import { stat, readFile } from 'fs/promises';
|
|
26
26
|
import { createInterface } from 'readline';
|
|
27
27
|
import { homedir, hostname, platform as osPlatform } from 'os';
|
|
@@ -29,7 +29,6 @@ import { join } from 'path';
|
|
|
29
29
|
import { getApiBaseOverride, getConfig } from '../config.js';
|
|
30
30
|
import { getProjectsRoot } from './paths.js';
|
|
31
31
|
import { setupClaudeHooks, setupClaudeMd, setupAgentsMd, setupGitignore, DEFAULT_SYNC_IGNORE } from '../setup.js';
|
|
32
|
-
import { sync } from '../sync.js';
|
|
33
32
|
import { getAuth, readAuthFresh } from '../auth.js';
|
|
34
33
|
import { post } from '../api.js';
|
|
35
34
|
import * as state from './state.js';
|
|
@@ -52,6 +51,9 @@ const BACKOFF_BASE_MS = parseInt(process.env.GIPITY_RELAY_BACKOFF_BASE_MS || '10
|
|
|
52
51
|
const BACKOFF_MAX_MS = parseInt(process.env.GIPITY_RELAY_BACKOFF_MAX_MS || '30000', 10);
|
|
53
52
|
const CANCEL_POLL_INTERVAL_MS = parseInt(process.env.GIPITY_RELAY_CANCEL_POLL_MS || '3000', 10);
|
|
54
53
|
const MAX_CONCURRENT_DISPATCHES = Math.max(1, parseInt(process.env.GIPITY_RELAY_MAX_CONCURRENT || '6', 10));
|
|
54
|
+
// Cap how long the pre-Claude project sync (and the post-dispatch push-back) may
|
|
55
|
+
// run before we kill it - a stalled sync must never hang a dispatch forever.
|
|
56
|
+
const PROJECT_SYNC_TIMEOUT_MS = parseInt(process.env.GIPITY_RELAY_SYNC_TIMEOUT_MS || '120000', 10);
|
|
55
57
|
// ─── HTTP helpers ──────────────────────────────────────────────────────
|
|
56
58
|
// Device-auth fetch lives in ./device-http.ts - shared with the capture
|
|
57
59
|
// hook runner so both POST to /remote-sessions/:convGuid/ingest with the
|
|
@@ -214,13 +216,13 @@ export async function run(opts = {}) {
|
|
|
214
216
|
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
215
217
|
if (opts.maxRunMs)
|
|
216
218
|
setTimeout(() => shutdown('maxRunMs'), opts.maxRunMs).unref();
|
|
217
|
-
// Take the PID lock.
|
|
218
|
-
//
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
log('info', 'another daemon is already running - exiting'
|
|
219
|
+
// Take the PID lock. Only a *live* daemon should block us: a leftover pid file
|
|
220
|
+
// from an unclean exit (container SIGKILL'd, or `--restart` brought us back on
|
|
221
|
+
// the same filesystem) must not trap us in a permanent restart loop.
|
|
222
|
+
// isDaemonRunning() validates the recorded PID is actually alive and clears
|
|
223
|
+
// the file if it's stale, so writeDaemonPid below can then take the lock.
|
|
224
|
+
if (state.isDaemonRunning()) {
|
|
225
|
+
log('info', 'another daemon is already running - exiting');
|
|
224
226
|
if (opts.verbose) {
|
|
225
227
|
process.stderr.write('Another relay daemon is already running (likely the autostarted one).\n' +
|
|
226
228
|
'Stop it first, then retry: gipity relay autostart uninstall (or stop the service),\n' +
|
|
@@ -228,6 +230,14 @@ export async function run(opts = {}) {
|
|
|
228
230
|
}
|
|
229
231
|
return 0;
|
|
230
232
|
}
|
|
233
|
+
try {
|
|
234
|
+
state.writeDaemonPid(process.pid);
|
|
235
|
+
}
|
|
236
|
+
catch (err) {
|
|
237
|
+
// Lost a genuine race with another daemon starting at the same instant.
|
|
238
|
+
log('info', 'lost pid-lock race with another daemon - exiting', { err: err?.message });
|
|
239
|
+
return 0;
|
|
240
|
+
}
|
|
231
241
|
const releasePid = () => state.clearDaemonPid();
|
|
232
242
|
process.on('exit', releasePid);
|
|
233
243
|
// Also release on our shutdown signals (exit handler sometimes doesn't fire).
|
|
@@ -582,15 +592,35 @@ async function handleDispatch(d) {
|
|
|
582
592
|
// serialization point that prevents that.
|
|
583
593
|
await killRunningForConv(d.conversation_guid);
|
|
584
594
|
let cwd;
|
|
595
|
+
let bootstrapped;
|
|
585
596
|
try {
|
|
586
|
-
cwd = await resolveCwdForProject(d);
|
|
587
|
-
log('debug', 'resolved project cwd', { id: d.short_guid, project: d.project_slug, cwd });
|
|
597
|
+
({ cwd, bootstrapped } = await resolveCwdForProject(d));
|
|
598
|
+
log('debug', 'resolved project cwd', { id: d.short_guid, project: d.project_slug, cwd, bootstrapped });
|
|
588
599
|
}
|
|
589
600
|
catch (err) {
|
|
590
601
|
log('error', 'could not resolve project cwd', { id: d.short_guid, err: err?.message });
|
|
591
602
|
await ack(d.short_guid, 'error', `Could not materialize project locally: ${err?.message || err}`);
|
|
592
603
|
return;
|
|
593
604
|
}
|
|
605
|
+
// Explicit, user-visible, timeout-bounded project sync BEFORE starting Claude -
|
|
606
|
+
// only on a freshly bootstrapped dir (the files aren't there yet). Pull the
|
|
607
|
+
// project's files first so Claude works against the real tree; a hung/slow sync
|
|
608
|
+
// is killed at PROJECT_SYNC_TIMEOUT_MS and reported instead of silently stalling
|
|
609
|
+
// the dispatch (the old in-process bootstrap sync could hang forever).
|
|
610
|
+
if (bootstrapped) {
|
|
611
|
+
await postIngest(d.conversation_guid, [{ kind: 'system', content: 'Syncing project files…' }]);
|
|
612
|
+
try {
|
|
613
|
+
await spawnSync(cwd, PROJECT_SYNC_TIMEOUT_MS);
|
|
614
|
+
await postIngest(d.conversation_guid, [{ kind: 'system', content: 'Project files synced.' }]);
|
|
615
|
+
}
|
|
616
|
+
catch (err) {
|
|
617
|
+
const msg = `project sync ${err?.message || 'failed'}`;
|
|
618
|
+
log('error', 'project sync failed - aborting dispatch', { id: d.short_guid, err: err?.message });
|
|
619
|
+
await postIngest(d.conversation_guid, [{ kind: 'system', content: `Claude Code not started - ${msg}` }]);
|
|
620
|
+
await ack(d.short_guid, 'error', msg);
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
594
624
|
// Build argv for `gipity claude -p …` (or with --resume). No shell - argv
|
|
595
625
|
// as array so the message string can't be interpreted as shell syntax.
|
|
596
626
|
//
|
|
@@ -690,7 +720,7 @@ async function handleDispatch(d) {
|
|
|
690
720
|
// this sync redundant for that case.
|
|
691
721
|
if (!spawnErr) {
|
|
692
722
|
try {
|
|
693
|
-
await spawnSync(cwd);
|
|
723
|
+
await spawnSync(cwd, PROJECT_SYNC_TIMEOUT_MS);
|
|
694
724
|
}
|
|
695
725
|
catch (err) {
|
|
696
726
|
log('warn', 'sync after dispatch failed', { id: d.short_guid, err: err?.message });
|
|
@@ -742,11 +772,11 @@ async function resolveCwdForProject(d) {
|
|
|
742
772
|
try {
|
|
743
773
|
const cfg = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
744
774
|
if (cfg.projectGuid === d.project_guid)
|
|
745
|
-
return path;
|
|
775
|
+
return { cwd: path, bootstrapped: false };
|
|
746
776
|
log('warn', 'project dir exists but guid mismatch - using it anyway', {
|
|
747
777
|
path, expected: d.project_guid, found: cfg.projectGuid,
|
|
748
778
|
});
|
|
749
|
-
return path;
|
|
779
|
+
return { cwd: path, bootstrapped: false };
|
|
750
780
|
}
|
|
751
781
|
catch { /* fall through to re-bootstrap */ }
|
|
752
782
|
}
|
|
@@ -771,17 +801,14 @@ async function resolveCwdForProject(d) {
|
|
|
771
801
|
setupClaudeMd();
|
|
772
802
|
setupAgentsMd();
|
|
773
803
|
setupGitignore();
|
|
774
|
-
try {
|
|
775
|
-
await sync({ interactive: false });
|
|
776
|
-
}
|
|
777
|
-
catch (err) {
|
|
778
|
-
log('warn', 'initial sync failed; project dir created but empty', { err: err?.message });
|
|
779
|
-
}
|
|
780
804
|
}
|
|
781
805
|
finally {
|
|
782
806
|
process.chdir(origCwd);
|
|
783
807
|
}
|
|
784
|
-
|
|
808
|
+
// Sync is NOT done here: the dispatch handler runs it as an explicit, visible,
|
|
809
|
+
// timeout-bounded step (a blocking in-process sync here used to hang the whole
|
|
810
|
+
// dispatch with no timeout and no user feedback).
|
|
811
|
+
return { cwd: path, bootstrapped: true };
|
|
785
812
|
}
|
|
786
813
|
const running = new Map();
|
|
787
814
|
export function getRunningDispatchGuids() {
|
|
@@ -821,7 +848,7 @@ export async function killRunningForConv(convGuid) {
|
|
|
821
848
|
* back to VFS. Runs as a child so we inherit sync's cwd-walk for config
|
|
822
849
|
* resolution (the daemon itself doesn't chdir into projects).
|
|
823
850
|
* Non-blocking on failure - caller catches and logs. */
|
|
824
|
-
async function spawnSync(cwd) {
|
|
851
|
+
async function spawnSync(cwd, timeoutMs) {
|
|
825
852
|
const cmd = process.env.GIPITY_RELAY_CLAUDE_CMD || 'gipity';
|
|
826
853
|
return new Promise((resolve, reject) => {
|
|
827
854
|
const child = spawn(cmd, ['sync', '--json'], {
|
|
@@ -832,18 +859,39 @@ async function spawnSync(cwd) {
|
|
|
832
859
|
// Drain pipes so the child doesn't stall on a full buffer.
|
|
833
860
|
let stdoutLen = 0;
|
|
834
861
|
let stderrBuf = '';
|
|
862
|
+
let settled = false;
|
|
863
|
+
let timer = null;
|
|
864
|
+
const finish = (fn) => { if (settled)
|
|
865
|
+
return; settled = true; if (timer)
|
|
866
|
+
clearTimeout(timer); fn(); };
|
|
867
|
+
// A sync that never returns must not hang the dispatch forever. Kill the child
|
|
868
|
+
// and clear its sync.lock (a SIGKILL'd `gipity sync` leaves the lock behind,
|
|
869
|
+
// which would make the next sync wait 30s on a dead holder).
|
|
870
|
+
if (timeoutMs) {
|
|
871
|
+
timer = setTimeout(() => {
|
|
872
|
+
try {
|
|
873
|
+
child.kill('SIGKILL');
|
|
874
|
+
}
|
|
875
|
+
catch { /* gone */ }
|
|
876
|
+
try {
|
|
877
|
+
unlinkSync(join(cwd, '.gipity', 'sync.lock'));
|
|
878
|
+
}
|
|
879
|
+
catch { /* not there */ }
|
|
880
|
+
finish(() => reject(new Error(`timed out after ${Math.round(timeoutMs / 1000)}s`)));
|
|
881
|
+
}, timeoutMs);
|
|
882
|
+
}
|
|
835
883
|
child.stdout?.on('data', (b) => { stdoutLen += b.length; });
|
|
836
884
|
child.stderr?.on('data', (b) => { stderrBuf += b.toString('utf-8'); });
|
|
837
|
-
child.on('error', (err) => reject(err));
|
|
838
|
-
child.on('exit', (code) => {
|
|
885
|
+
child.on('error', (err) => finish(() => reject(err)));
|
|
886
|
+
child.on('exit', (code) => finish(() => {
|
|
839
887
|
if (code === 0) {
|
|
840
|
-
log('info', 'sync
|
|
888
|
+
log('info', 'sync done', { cwd, stdoutLen });
|
|
841
889
|
resolve();
|
|
842
890
|
}
|
|
843
891
|
else {
|
|
844
892
|
reject(new Error(`gipity sync exited ${code}${stderrBuf ? `: ${stderrBuf.trim().slice(0, 300)}` : ''}`));
|
|
845
893
|
}
|
|
846
|
-
});
|
|
894
|
+
}));
|
|
847
895
|
});
|
|
848
896
|
}
|
|
849
897
|
export async function spawnGipityClaude(args, cwd, d) {
|
package/dist/relay/redact.js
CHANGED
|
@@ -30,15 +30,23 @@ const MIN_SECRET_LEN = 12;
|
|
|
30
30
|
* appearing in a relay transcript is effectively always a credential, so
|
|
31
31
|
* over-redaction risk is negligible. */
|
|
32
32
|
const JWT_RE = /eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g;
|
|
33
|
-
/**
|
|
34
|
-
*
|
|
33
|
+
/** Anthropic credential tokens: API keys (`sk-ant-api…`) and Claude Code OAuth
|
|
34
|
+
* tokens (`sk-ant-oat…`). These are NOT JWT-shaped, so the JWT backstop misses
|
|
35
|
+
* them. On a bring-your-own-key relay the user's own API key lives in the
|
|
36
|
+
* container env, and a `bypassPermissions` session could otherwise echo it
|
|
37
|
+
* (`env`, `cat`) into the transcript — so pattern-redact it regardless of the
|
|
38
|
+
* literal-secret list. An `sk-ant-` token in a relay transcript is always a
|
|
39
|
+
* credential, so over-redaction risk is negligible. */
|
|
40
|
+
const ANTHROPIC_KEY_RE = /sk-ant-[A-Za-z0-9_-]{20,}/g;
|
|
41
|
+
/** Replace every occurrence of each known secret - and any JWT- or
|
|
42
|
+
* Anthropic-key-shaped substring - in `text` with the marker. */
|
|
35
43
|
function redactString(text, secrets) {
|
|
36
44
|
let out = text;
|
|
37
45
|
for (const secret of secrets) {
|
|
38
46
|
if (out.includes(secret))
|
|
39
47
|
out = out.split(secret).join(REDACTION_MARKER);
|
|
40
48
|
}
|
|
41
|
-
return out.replace(JWT_RE, REDACTION_MARKER);
|
|
49
|
+
return out.replace(JWT_RE, REDACTION_MARKER).replace(ANTHROPIC_KEY_RE, REDACTION_MARKER);
|
|
42
50
|
}
|
|
43
51
|
/** Deep-walk any JSON-ish value, redacting every string. Returns a new
|
|
44
52
|
* value; objects/arrays are cloned, primitives passed through. */
|
package/dist/relay/state.js
CHANGED
|
@@ -116,8 +116,26 @@ export function isDaemonRunning() {
|
|
|
116
116
|
try {
|
|
117
117
|
const raw = readFileSync(RELAY_PID_FILE, 'utf-8').trim();
|
|
118
118
|
const pid = parseInt(raw, 10);
|
|
119
|
-
|
|
119
|
+
// A corrupt/empty pid file is stale - clear it so it can't trap a restart.
|
|
120
|
+
if (!pid || isNaN(pid)) {
|
|
121
|
+
try {
|
|
122
|
+
unlinkSync(RELAY_PID_FILE);
|
|
123
|
+
}
|
|
124
|
+
catch { /* ignore */ }
|
|
120
125
|
return false;
|
|
126
|
+
}
|
|
127
|
+
// Our OWN pid in the file = stale from a previous incarnation, NOT a live peer.
|
|
128
|
+
// In a container the daemon is always pid 1, so `--restart` brings us back as
|
|
129
|
+
// pid 1 with the dead run's relay.pid (also 1) left behind; process.kill(1,0)
|
|
130
|
+
// would say "alive" (it's us) and trap us in a permanent restart loop. We write
|
|
131
|
+
// our pid only AFTER this check, so finding it here means the file predates us.
|
|
132
|
+
if (pid === process.pid) {
|
|
133
|
+
try {
|
|
134
|
+
unlinkSync(RELAY_PID_FILE);
|
|
135
|
+
}
|
|
136
|
+
catch { /* ignore */ }
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
121
139
|
// `kill 0` sends no signal but checks if the PID is addressable.
|
|
122
140
|
process.kill(pid, 0);
|
|
123
141
|
return true;
|
package/dist/setup.js
CHANGED
|
@@ -2,11 +2,9 @@
|
|
|
2
2
|
* Shared project setup helpers used by both `init` and `claude`.
|
|
3
3
|
*/
|
|
4
4
|
import { resolve, join, dirname } from 'path';
|
|
5
|
-
import {
|
|
5
|
+
import { homedir } from 'os';
|
|
6
6
|
import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'fs';
|
|
7
|
-
import { SCAFFOLD_HOOK_WARNING } from './prompts.js';
|
|
8
7
|
import { SKILLS_CONTENT, BUILD_VS_NON_BUILD_RULE, DEFINITION_OF_DONE } from './knowledge.js';
|
|
9
|
-
import { getConfig } from './config.js';
|
|
10
8
|
export { SKILLS_CONTENT };
|
|
11
9
|
/** Canonical list of workstation artifacts that are NOT part of the project.
|
|
12
10
|
* Used as the single source of truth for three separate decisions:
|
|
@@ -91,116 +89,122 @@ export const PERMISSIONS_SETTINGS = {
|
|
|
91
89
|
],
|
|
92
90
|
},
|
|
93
91
|
};
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
92
|
+
// Hooks now ship in the Gipity Claude Code plugin (GipityAI/claude-plugin,
|
|
93
|
+
// which doubles as its own marketplace): file sync (push on edit, pull on
|
|
94
|
+
// prompt) and `gipity claude` session capture, every script guarded to no-op
|
|
95
|
+
// outside Gipity projects. Past CLI versions wrote these hook blocks directly
|
|
96
|
+
// into each project's .claude/settings.json with absolute paths baked in -
|
|
97
|
+
// that left orphaned entries behind on uninstall (the CLI keeps no inventory
|
|
98
|
+
// of projects it touched) and could even land in the user-global settings
|
|
99
|
+
// when a gipity command ran from $HOME. The plugin replaces all of it with
|
|
100
|
+
// one declarative enablement entry: Claude Code resolves script paths via
|
|
101
|
+
// ${CLAUDE_PLUGIN_ROOT}, fetches the marketplace non-interactively at launch
|
|
102
|
+
// (headless included), and uninstall/disable removes every hook at once.
|
|
103
|
+
export const GIPITY_PLUGIN_ID = 'gipity@gipity';
|
|
104
|
+
export const GIPITY_MARKETPLACE_NAME = 'gipity';
|
|
105
|
+
export const GIPITY_MARKETPLACE_REPO = 'GipityAI/claude-plugin';
|
|
106
|
+
/** True for hook commands the CLI itself wrote into settings.json in past
|
|
107
|
+
* versions. Matched by signature so migration strips exactly our own
|
|
108
|
+
* entries and never touches user-authored hooks. */
|
|
109
|
+
export function isGipityManagedHookCommand(command) {
|
|
110
|
+
return (
|
|
111
|
+
// Capture hooks: bare absolute runner path or the fire-time launcher.
|
|
112
|
+
command.includes('capture-runner.js') ||
|
|
113
|
+
// File-sync push one-liner (spawn('gipity',['push',p,'--quiet'],...)).
|
|
114
|
+
command.includes("'gipity',['push'") ||
|
|
115
|
+
// Pull-on-prompt one-liner, current and older variants.
|
|
116
|
+
command.includes('gipity sync --json') ||
|
|
117
|
+
command.includes('gipity sync down --json') ||
|
|
118
|
+
// Scaffold nudge (retired entirely - CLAUDE.md carries the rule).
|
|
119
|
+
command.includes("['gipity.yaml','src','functions','package.json']"));
|
|
103
120
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
}],
|
|
134
|
-
},
|
|
135
|
-
],
|
|
136
|
-
PostToolUse: [
|
|
137
|
-
{
|
|
138
|
-
// File sync for Write/Edit - push any edited file back to the
|
|
139
|
-
// cloud workspace so web previews see the change.
|
|
140
|
-
matcher: 'Write|Edit',
|
|
141
|
-
hooks: [{
|
|
142
|
-
type: 'command',
|
|
143
|
-
command: `node -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{try{const p=JSON.parse(d).tool_input?.file_path;if(!p||!require('fs').existsSync('.gipity.json'))process.exit(0);require('child_process').spawn('gipity',['push',p,'--quiet'],{stdio:'ignore',detached:true,shell:true}).unref()}catch{}})"`,
|
|
144
|
-
}],
|
|
145
|
-
},
|
|
146
|
-
],
|
|
147
|
-
UserPromptSubmit: [
|
|
148
|
-
{
|
|
149
|
-
// Pull down any cloud-side file changes before the next turn so
|
|
150
|
-
// the agent sees current state.
|
|
151
|
-
matcher: '',
|
|
152
|
-
hooks: [{
|
|
153
|
-
type: 'command',
|
|
154
|
-
command: `node -e "if(!require('fs').existsSync('.gipity.json'))process.exit(0);require('child_process').exec('gipity sync --json',(e,o)=>{if(e)process.exit(0);try{const r=JSON.parse(o);if(r.applied>0)console.log(JSON.stringify({systemMessage:'Gipity sync: '+(r.summary||'Files changed.')}))}catch{}})"`,
|
|
155
|
-
}],
|
|
156
|
-
},
|
|
157
|
-
],
|
|
158
|
-
},
|
|
159
|
-
};
|
|
160
|
-
/** Build the four lifecycle-hook entries that invoke the bundled capture
|
|
161
|
-
* runner. Kept as a factory rather than a constant so the absolute
|
|
162
|
-
* runner path is resolved at install time, reflecting the CLI's current
|
|
163
|
-
* install location. */
|
|
164
|
-
function buildCaptureHookEntries(source) {
|
|
165
|
-
const runner = resolveCaptureRunnerPath();
|
|
166
|
-
const cmd = (event) => `node ${JSON.stringify(runner)} ${source} ${event}`;
|
|
167
|
-
return {
|
|
168
|
-
SessionStart: [{ hooks: [{ type: 'command', command: cmd('session-start') }] }],
|
|
169
|
-
Stop: [{ hooks: [{ type: 'command', command: cmd('stop') }] }],
|
|
170
|
-
SubagentStop: [{ hooks: [{ type: 'command', command: cmd('subagent-stop') }] }],
|
|
171
|
-
SessionEnd: [{ hooks: [{ type: 'command', command: cmd('session-end') }] }],
|
|
172
|
-
// Mid-run incremental flush (matcher '' = every tool). Stop/SessionEnd only
|
|
173
|
-
// fire on a CLEAN exit, so without this a session that's killed/crashes mid-run
|
|
174
|
-
// (e.g. a long headless build that times out) loses its whole transcript. The
|
|
175
|
-
// runner throttles this so it's cheap. Merged alongside the file-sync PostToolUse.
|
|
176
|
-
PostToolUse: [{ matcher: '', hooks: [{ type: 'command', command: cmd('post-tool-use') }] }],
|
|
177
|
-
};
|
|
121
|
+
/** Remove Gipity-managed hook entries from a parsed settings object,
|
|
122
|
+
* preserving user-authored hooks untouched. Returns true if anything
|
|
123
|
+
* was removed. Exported for tests and uninstall. */
|
|
124
|
+
export function stripGipityHooks(settings) {
|
|
125
|
+
const hooks = settings.hooks;
|
|
126
|
+
if (!hooks || typeof hooks !== 'object')
|
|
127
|
+
return false;
|
|
128
|
+
let changed = false;
|
|
129
|
+
for (const [event, groups] of Object.entries(hooks)) {
|
|
130
|
+
if (!Array.isArray(groups))
|
|
131
|
+
continue;
|
|
132
|
+
const kept = groups
|
|
133
|
+
.map((group) => {
|
|
134
|
+
if (!Array.isArray(group?.hooks))
|
|
135
|
+
return group;
|
|
136
|
+
const remaining = group.hooks.filter((h) => !(typeof h?.command === 'string' && isGipityManagedHookCommand(h.command)));
|
|
137
|
+
if (remaining.length !== group.hooks.length)
|
|
138
|
+
changed = true;
|
|
139
|
+
return { ...group, hooks: remaining };
|
|
140
|
+
})
|
|
141
|
+
.filter((group) => !Array.isArray(group?.hooks) || group.hooks.length > 0);
|
|
142
|
+
if (kept.length === 0)
|
|
143
|
+
delete hooks[event];
|
|
144
|
+
else
|
|
145
|
+
hooks[event] = kept;
|
|
146
|
+
}
|
|
147
|
+
if (Object.keys(hooks).length === 0)
|
|
148
|
+
delete settings.hooks;
|
|
149
|
+
return changed;
|
|
178
150
|
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
151
|
+
function readSettingsFile(path) {
|
|
152
|
+
if (!existsSync(path))
|
|
153
|
+
return {};
|
|
154
|
+
try {
|
|
155
|
+
return JSON.parse(readFileSync(path, 'utf-8'));
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
return {}; // corrupted - start fresh rather than crash setup
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/** Ensure the Gipity plugin is enabled at user scope (~/.claude/settings.json)
|
|
162
|
+
* via the documented declarative keys: register the marketplace under
|
|
163
|
+
* `extraKnownMarketplaces` and enable the plugin under `enabledPlugins`.
|
|
164
|
+
* Claude Code fetches both non-interactively at next launch. An explicit
|
|
165
|
+
* user disable (`"gipity@gipity": false`) is respected unless `force` -
|
|
166
|
+
* the user said no, and `gipity status --repair-hooks` is the deliberate
|
|
167
|
+
* way to say yes again. Also strips legacy Gipity hook blocks that older
|
|
168
|
+
* CLI versions left in the user-global settings. */
|
|
169
|
+
export function ensureGipityPlugin(force = false) {
|
|
170
|
+
const claudeDir = join(homedir(), '.claude');
|
|
182
171
|
const settingsPath = join(claudeDir, 'settings.json');
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
172
|
+
const settings = readSettingsFile(settingsPath);
|
|
173
|
+
let changed = stripGipityHooks(settings);
|
|
174
|
+
const marketplaces = settings.extraKnownMarketplaces ?? (settings.extraKnownMarketplaces = {});
|
|
175
|
+
if (!marketplaces[GIPITY_MARKETPLACE_NAME]) {
|
|
176
|
+
marketplaces[GIPITY_MARKETPLACE_NAME] = {
|
|
177
|
+
source: { source: 'github', repo: GIPITY_MARKETPLACE_REPO },
|
|
178
|
+
};
|
|
179
|
+
changed = true;
|
|
191
180
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
const captureEntries = captureEnabled ? buildCaptureHookEntries('claude-code') : {};
|
|
197
|
-
// Merge per-event arrays rather than spread-overwriting: capture and file-sync
|
|
198
|
-
// both register PostToolUse, and a plain spread would drop one of them.
|
|
199
|
-
const mergedHooks = { ...HOOKS_SETTINGS.hooks };
|
|
200
|
-
for (const [event, entries] of Object.entries(captureEntries)) {
|
|
201
|
-
mergedHooks[event] = [...(mergedHooks[event] ?? []), ...entries];
|
|
181
|
+
const enabled = settings.enabledPlugins ?? (settings.enabledPlugins = {});
|
|
182
|
+
if (enabled[GIPITY_PLUGIN_ID] !== true && (force || !(GIPITY_PLUGIN_ID in enabled))) {
|
|
183
|
+
enabled[GIPITY_PLUGIN_ID] = true;
|
|
184
|
+
changed = true;
|
|
202
185
|
}
|
|
203
|
-
|
|
186
|
+
if (!changed)
|
|
187
|
+
return;
|
|
188
|
+
mkdirSync(claudeDir, { recursive: true });
|
|
189
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
190
|
+
}
|
|
191
|
+
export function setupClaudeHooks() {
|
|
192
|
+
// All hooks ship in the plugin - enable it at user scope (and clean up any
|
|
193
|
+
// legacy hook blocks in the user-global settings while we're there).
|
|
194
|
+
ensureGipityPlugin();
|
|
195
|
+
// Never treat the home directory as a project. A gipity command run from
|
|
196
|
+
// $HOME used to write "project" hooks straight into the user-global
|
|
197
|
+
// ~/.claude/settings.json; permissions are project-scoped, so at $HOME
|
|
198
|
+
// there is nothing project-level left to do.
|
|
199
|
+
const cwd = resolve(process.cwd());
|
|
200
|
+
if (cwd === resolve(homedir()))
|
|
201
|
+
return;
|
|
202
|
+
const claudeDir = join(cwd, '.claude');
|
|
203
|
+
mkdirSync(claudeDir, { recursive: true });
|
|
204
|
+
const settingsPath = join(claudeDir, 'settings.json');
|
|
205
|
+
const settings = readSettingsFile(settingsPath);
|
|
206
|
+
// Migration: remove the hook blocks older CLI versions wrote here.
|
|
207
|
+
stripGipityHooks(settings);
|
|
204
208
|
// Merge permissions (additive - preserve user's existing allows)
|
|
205
209
|
const perms = settings.permissions || {};
|
|
206
210
|
if (!perms.allow)
|
package/dist/sync.js
CHANGED
|
@@ -455,17 +455,35 @@ async function applyUpload(projectGuid, root, a, onConflict) {
|
|
|
455
455
|
throw err;
|
|
456
456
|
}
|
|
457
457
|
}
|
|
458
|
+
/** Name of the optional per-project ignore file (gitignore-style: one pattern
|
|
459
|
+
* per line, blank lines and `#` comments skipped). Patterns use the same
|
|
460
|
+
* matcher as the config `ignore` list (see shouldIgnore) and let research
|
|
461
|
+
* artifacts, scratch data, or vendored references live inside the project
|
|
462
|
+
* directory without being synced (and therefore without being deployed). */
|
|
463
|
+
export const GIPITY_IGNORE_FILE = '.gipityignore';
|
|
464
|
+
export function readGipityIgnore(root) {
|
|
465
|
+
const path = join(root, GIPITY_IGNORE_FILE);
|
|
466
|
+
if (!existsSync(path))
|
|
467
|
+
return [];
|
|
468
|
+
return readFileSync(path, 'utf8')
|
|
469
|
+
.split('\n')
|
|
470
|
+
.map(line => line.trim())
|
|
471
|
+
.filter(line => line && !line.startsWith('#'))
|
|
472
|
+
.map(line => line.replace(/^\.\//, '').replace(/^\//, ''));
|
|
473
|
+
}
|
|
474
|
+
/** The ignore list a sync/push actually runs with: the project config's
|
|
475
|
+
* `ignore` (falling back to DEFAULT_SYNC_IGNORE when empty, so an empty list
|
|
476
|
+
* never means "sync everything - node_modules, .git and all"), plus any
|
|
477
|
+
* `.gipityignore` patterns. The ignore file itself never syncs. */
|
|
478
|
+
export function effectiveIgnore(root, configIgnore) {
|
|
479
|
+
const base = configIgnore && configIgnore.length ? configIgnore : DEFAULT_SYNC_IGNORE;
|
|
480
|
+
return [...base, GIPITY_IGNORE_FILE, ...readGipityIgnore(root)];
|
|
481
|
+
}
|
|
458
482
|
export async function sync(opts = {}) {
|
|
459
483
|
const config = requireConfig();
|
|
460
484
|
const root = projectDir();
|
|
461
485
|
const interactive = opts.interactive ?? process.stdout.isTTY ?? false;
|
|
462
|
-
|
|
463
|
-
// by the one-off Home-fallback path) would make sync walk and hash the
|
|
464
|
-
// entire tree - node_modules, .git, caches and all. Fall back to the
|
|
465
|
-
// standard ignore set so an empty list never means "sync everything".
|
|
466
|
-
const ignore = config.ignore && config.ignore.length
|
|
467
|
-
? config.ignore
|
|
468
|
-
: [...DEFAULT_SYNC_IGNORE];
|
|
486
|
+
const ignore = effectiveIgnore(root, config.ignore);
|
|
469
487
|
const releaseLock = await acquireLock();
|
|
470
488
|
try {
|
|
471
489
|
return await syncInner(config.projectGuid, root, ignore, opts, interactive);
|
|
@@ -484,6 +502,15 @@ async function syncInner(projectGuid, root, ignore, opts, interactive) {
|
|
|
484
502
|
const local = walkLocal(root, ignore, baseline.files);
|
|
485
503
|
p?.phase('Checking Gipity for changes…');
|
|
486
504
|
const remote = await fetchRemote(projectGuid);
|
|
505
|
+
// Ignored paths are invisible on BOTH sides: filtering only the local walk
|
|
506
|
+
// would classify a remote copy as "added" (pull it), then next pass as a
|
|
507
|
+
// local deletion (delete it remotely) - a churn loop. Filtering remote too
|
|
508
|
+
// means a path that synced before it was ignored just stays put remotely
|
|
509
|
+
// (delete it explicitly if it shouldn't be deployed).
|
|
510
|
+
for (const path of [...remote.keys()]) {
|
|
511
|
+
if (shouldIgnore(path, ignore))
|
|
512
|
+
remote.delete(path);
|
|
513
|
+
}
|
|
487
514
|
// Hash everything we might classify ambiguously. Any local path also on
|
|
488
515
|
// remote (and the remote has a hash) needs a local hash so size-match-but-
|
|
489
516
|
// content-differs isn't misclassified. Anything in baseline that's still
|
|
@@ -792,7 +819,7 @@ export async function pushFile(filePath) {
|
|
|
792
819
|
const config = requireConfig();
|
|
793
820
|
const root = projectDir();
|
|
794
821
|
const rel = relative(root, filePath).replace(/\\/g, '/');
|
|
795
|
-
if (shouldIgnore(rel, config.ignore))
|
|
822
|
+
if (shouldIgnore(rel, effectiveIgnore(root, config.ignore)))
|
|
796
823
|
return;
|
|
797
824
|
const baseline = readBaseline(config.projectGuid);
|
|
798
825
|
const baseEntry = baseline.files[rel];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gipity",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.384",
|
|
4
4
|
"description": "The full-stack platform tuned for AI agents. Database, storage, auth, functions, deploy, and drop-in kits - all agent-tuned. Pair with Claude Code or use standalone.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"gipity": "dist/updater/shim.js",
|