gipity 1.0.407 → 1.0.409
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/banner.js +16 -1
- package/dist/commands/add.js +1 -1
- package/dist/commands/approval.js +1 -1
- package/dist/commands/claude.js +9 -9
- package/dist/commands/email.js +2 -1
- package/dist/commands/gmail.js +2 -1
- package/dist/commands/init.js +2 -1
- package/dist/commands/job.js +2 -1
- package/dist/commands/login.js +1 -0
- package/dist/commands/notify.js +2 -1
- package/dist/commands/page.js +1 -1
- package/dist/commands/payments.js +2 -1
- package/dist/commands/plan.js +1 -1
- package/dist/commands/remove.js +1 -1
- package/dist/commands/secrets.js +2 -1
- package/dist/commands/service.js +2 -1
- package/dist/commands/sync.js +2 -1
- package/dist/commands/test.js +6 -0
- package/dist/commands/text.js +2 -1
- package/dist/commands/token.js +2 -1
- package/dist/config.js +4 -4
- package/dist/helpers/output.js +42 -13
- package/dist/knowledge.js +2 -0
- package/dist/platform.js +53 -6
- package/dist/relay/daemon.js +8 -30
- package/dist/relay/installers.js +10 -2
- package/dist/setup.js +4 -4
- package/dist/updater/bootstrap.js +3 -4
- package/dist/updater/check.js +2 -3
- package/package.json +3 -4
- package/dist/coding-guidelines.js +0 -52
- package/dist/commands/browser.js +0 -84
- package/dist/commands/checkpoint.js +0 -68
- package/dist/commands/hook-capture.js +0 -215
- package/dist/commands/scaffold.js +0 -58
- package/dist/commands/skills.js +0 -45
- package/dist/commands/start-cc.js +0 -313
- package/dist/gip3dw-guide.js +0 -18
- package/dist/platform-overview.js +0 -52
- package/dist/relay/transcripts.js +0 -119
- package/hooks/post-write.sh +0 -17
- package/hooks/pre-turn.sh +0 -20
package/dist/banner.js
CHANGED
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
// ── Gipity CLI Startup Banner ──────────────────────────────────────────
|
|
2
2
|
// Two-panel box showing all AI models, platform tools, and sandbox capabilities.
|
|
3
|
+
import { homedir } from 'os';
|
|
4
|
+
import { sep } from 'path';
|
|
3
5
|
import { brand, bold, faint, muted, fg } from './colors.js';
|
|
6
|
+
/** Abbreviate a home-relative path to `~`, cross-platform. On Windows `HOME`
|
|
7
|
+
* is usually unset (it's `USERPROFILE`), and `str.replace('', '~')` prepends
|
|
8
|
+
* a stray `~`, so use os.homedir() and a real prefix check. */
|
|
9
|
+
function tildify(p) {
|
|
10
|
+
const home = homedir();
|
|
11
|
+
if (!home)
|
|
12
|
+
return p;
|
|
13
|
+
if (p === home)
|
|
14
|
+
return '~';
|
|
15
|
+
if (p.startsWith(home + sep))
|
|
16
|
+
return '~' + p.slice(home.length);
|
|
17
|
+
return p;
|
|
18
|
+
}
|
|
4
19
|
// ── Static content ─────────────────────────────────────────────────────
|
|
5
20
|
// ── Feature groups ────────────────────────────────────────────────────
|
|
6
21
|
const AI_MODELS = [
|
|
@@ -174,7 +189,7 @@ function buildLeftPanel(opts, panelW) {
|
|
|
174
189
|
leftLines.push(center(line, panelW));
|
|
175
190
|
}
|
|
176
191
|
if (opts.cwd) {
|
|
177
|
-
const short = leadingEllipsify(opts.cwd
|
|
192
|
+
const short = leadingEllipsify(tildify(opts.cwd), panelW);
|
|
178
193
|
leftLines.push('');
|
|
179
194
|
leftLines.push(center(muted(short), panelW));
|
|
180
195
|
leftLines.push('');
|
package/dist/commands/add.js
CHANGED
|
@@ -144,7 +144,7 @@ function buildLocalPayload(rootDir) {
|
|
|
144
144
|
return { name: path.basename(rootDir), files };
|
|
145
145
|
}
|
|
146
146
|
export const addCommand = new Command('add')
|
|
147
|
-
.description('Add a template
|
|
147
|
+
.description('Add a template or kit')
|
|
148
148
|
.argument('[name]', 'Template/kit key, OR a local directory path (./, ~/, or /abs). Omit for help; use --list for just the catalog.')
|
|
149
149
|
.option('--title <title>', 'App title - templates only (defaults to project name)')
|
|
150
150
|
.option('--description <desc>', 'App description for meta tags - templates only')
|
|
@@ -21,7 +21,7 @@ function keyToIndex(key) {
|
|
|
21
21
|
return code >= 0 && code < 26 ? code : -1;
|
|
22
22
|
}
|
|
23
23
|
export const approvalCommand = new Command('approval')
|
|
24
|
-
.description('
|
|
24
|
+
.description('Manage approvals');
|
|
25
25
|
approvalCommand
|
|
26
26
|
.command('list')
|
|
27
27
|
.description('List pending approvals')
|
package/dist/commands/claude.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
-
import { join, dirname, resolve, basename } from 'path';
|
|
2
|
+
import { join, dirname, resolve, basename, sep } from 'path';
|
|
3
3
|
import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync, readdirSync, statSync } from 'fs';
|
|
4
|
-
import { spawn } from 'child_process';
|
|
5
4
|
import { homedir } from 'os';
|
|
6
5
|
import { fileURLToPath } from 'url';
|
|
7
|
-
import { resolveCommand } from '../platform.js';
|
|
6
|
+
import { resolveCommand, spawnCommand } from '../platform.js';
|
|
8
7
|
import { getAuth, saveAuth, sessionExpired, accessTokenExpired, refreshTokenIfNeeded } from '../auth.js';
|
|
9
8
|
import { get, post, publicPost, ApiError, getAccountSlug } from '../api.js';
|
|
10
9
|
import { getConfig, saveConfigAt, clearConfigCache, getApiBaseOverride, DEFAULT_API_BASE, getConfigPath } from '../config.js';
|
|
@@ -234,7 +233,7 @@ export const claudeCommand = new Command('claude')
|
|
|
234
233
|
// Forwarded to `claude` via the unknown-arg passthrough below (NOT in the
|
|
235
234
|
// gipity strip lists). Declared here only so it shows up in --help — without
|
|
236
235
|
// this, callers can't discover that the session model is selectable.
|
|
237
|
-
.option('--model <model>', 'Model for the Claude session, forwarded to claude (e.g. sonnet, opus, or a full id like claude-sonnet-
|
|
236
|
+
.option('--model <model>', 'Model for the Claude session, forwarded to claude (e.g. sonnet, opus, or a full id like claude-sonnet-5)')
|
|
238
237
|
.allowUnknownOption(true)
|
|
239
238
|
.allowExcessArguments(true)
|
|
240
239
|
.action(async (opts) => {
|
|
@@ -822,8 +821,9 @@ export const claudeCommand = new Command('claude')
|
|
|
822
821
|
allArgs.push('--verbose');
|
|
823
822
|
}
|
|
824
823
|
}
|
|
825
|
-
// Resolve
|
|
826
|
-
//
|
|
824
|
+
// Resolve the real path on Windows; spawnCommand then routes the `.cmd`
|
|
825
|
+
// shim through cmd.exe with verbatim args (a bare `spawn` of a `.cmd`
|
|
826
|
+
// throws EINVAL on Node >=18.20.2/20.12.2 - the CVE-2024-27980 fix).
|
|
827
827
|
const claudeCmd = resolveCommand('claude');
|
|
828
828
|
const childEnv = { ...process.env };
|
|
829
829
|
// Gate hook-based capture: when the daemon is streaming via
|
|
@@ -844,7 +844,7 @@ export const claudeCommand = new Command('claude')
|
|
|
844
844
|
// `-p` runs skip the dialog already, so this is only needed interactively.
|
|
845
845
|
if (!nonInteractive)
|
|
846
846
|
markFolderTrusted(process.cwd());
|
|
847
|
-
const child =
|
|
847
|
+
const child = spawnCommand(claudeCmd, allArgs, {
|
|
848
848
|
stdio: 'inherit',
|
|
849
849
|
cwd: process.cwd(),
|
|
850
850
|
env: childEnv,
|
|
@@ -945,10 +945,10 @@ function formatNewProjectLabel(existingSlugs) {
|
|
|
945
945
|
const slug = suggestProjectName(existingSlugs);
|
|
946
946
|
const root = getProjectsRoot();
|
|
947
947
|
const home = homedir();
|
|
948
|
-
const display = root.startsWith(home +
|
|
948
|
+
const display = root === home || root.startsWith(home + sep)
|
|
949
949
|
? '~' + root.slice(home.length)
|
|
950
950
|
: root;
|
|
951
|
-
return `${display}
|
|
951
|
+
return `${display}${sep}${slug}`;
|
|
952
952
|
}
|
|
953
953
|
/** Run the size-tier scan, surface the right UX:
|
|
954
954
|
* - easy → silent, return true.
|
package/dist/commands/email.js
CHANGED
|
@@ -7,7 +7,8 @@ function collect(value, prev) {
|
|
|
7
7
|
return [...prev, value];
|
|
8
8
|
}
|
|
9
9
|
export const emailCommand = new Command('email')
|
|
10
|
-
.description('Send email
|
|
10
|
+
.description('Send email')
|
|
11
|
+
.addHelpText('after', '\nSends from gipity@gipity.ai. Omit --to to self-send.')
|
|
11
12
|
.requiredOption('--subject <subject>', 'Email subject')
|
|
12
13
|
.requiredOption('--body <body>', 'Email body (plain text)')
|
|
13
14
|
.option('--to <email>', 'Recipient (repeatable; omit for self-send)', collect, [])
|
package/dist/commands/gmail.js
CHANGED
|
@@ -3,7 +3,8 @@ import { get, post } from '../api.js';
|
|
|
3
3
|
import { run, printList, printResult } from '../helpers/index.js';
|
|
4
4
|
import { muted } from '../colors.js';
|
|
5
5
|
export const gmailCommand = new Command('gmail')
|
|
6
|
-
.description('
|
|
6
|
+
.description('Use your Gmail')
|
|
7
|
+
.addHelpText('after', '\nSend, reply, search, or read via your own Gmail (separate from `gipity email`).');
|
|
7
8
|
gmailCommand
|
|
8
9
|
.command('search <query...>')
|
|
9
10
|
.description('Search your Gmail (standard Gmail search syntax)')
|
package/dist/commands/init.js
CHANGED
|
@@ -22,7 +22,8 @@ function resolveTools(forFlag) {
|
|
|
22
22
|
return SUPPORTED_TOOLS.filter(t => requested.includes(t.key) || (!t.optIn && requested.includes('all')));
|
|
23
23
|
}
|
|
24
24
|
export const initCommand = new Command('init')
|
|
25
|
-
.description('Link this directory to a
|
|
25
|
+
.description('Link this directory to a project')
|
|
26
|
+
.addHelpText('after', '\nWrites CLAUDE.md/AGENTS.md primer files so your AI coding tool understands Gipity.')
|
|
26
27
|
.argument('[name]', 'Project name/slug (defaults to current directory name)')
|
|
27
28
|
.option('--agent <guid>', 'Agent GUID to use')
|
|
28
29
|
.option('--for <tools>', `Which AI tool primer files to write (comma-separated). Default: all except aider (opt-in - it also writes .aider.conf.yml). Choices: ${TOOL_KEYS.join(', ')}, all`)
|
package/dist/commands/job.js
CHANGED
|
@@ -4,7 +4,8 @@ import { requireConfig } from '../config.js';
|
|
|
4
4
|
import { error as clrError, bold, muted, success, warning as warn } from '../colors.js';
|
|
5
5
|
import { run, printList } from '../helpers/index.js';
|
|
6
6
|
export const jobCommand = new Command('job')
|
|
7
|
-
.description('
|
|
7
|
+
.description('Run long jobs (CPU/GPU)')
|
|
8
|
+
.addHelpText('after', '\nCPU sandbox or GPU (gpu-small=L4, gpu-medium=A10G, gpu-large=A100, gpu-huge=H100). Declared in the gipity.yaml jobs: phase; submit and poll via the subcommands below.');
|
|
8
9
|
jobCommand
|
|
9
10
|
.command('list')
|
|
10
11
|
.description('List jobs')
|
package/dist/commands/login.js
CHANGED
package/dist/commands/notify.js
CHANGED
|
@@ -4,7 +4,8 @@ import { resolveProjectContext } from '../config.js';
|
|
|
4
4
|
import { bold, muted, success, warning } from '../colors.js';
|
|
5
5
|
import { run } from '../helpers/index.js';
|
|
6
6
|
export const notifyCommand = new Command('notify')
|
|
7
|
-
.description('
|
|
7
|
+
.description('Web push notifications')
|
|
8
|
+
.addHelpText('after', '\nGipity Notify: send a test notification or inspect subscriptions.');
|
|
8
9
|
notifyCommand
|
|
9
10
|
.command('test')
|
|
10
11
|
.description('Send a test push notification to yourself, a user, or everyone')
|
package/dist/commands/page.js
CHANGED
|
@@ -10,7 +10,7 @@ import { pageFetchCommand } from './page-fetch.js';
|
|
|
10
10
|
// top-level surface lean and makes the siblings discoverable via `page --help`.
|
|
11
11
|
// `inspect` is the rendered DOM (browser); `fetch` is the raw asset (plain HTTP).
|
|
12
12
|
export const pageCommand = new Command('page')
|
|
13
|
-
.description('Inspect
|
|
13
|
+
.description('Inspect web pages')
|
|
14
14
|
.addCommand(pageInspectCommand)
|
|
15
15
|
.addCommand(pageEvalCommand)
|
|
16
16
|
.addCommand(pageScreenshotCommand)
|
|
@@ -19,7 +19,8 @@ function renderStatus(status) {
|
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
21
|
export const paymentsCommand = new Command('payments')
|
|
22
|
-
.description('
|
|
22
|
+
.description('Charge your users via Stripe')
|
|
23
|
+
.addHelpText('after', '\nAccept one-time purchases and subscriptions. Set up with `gipity payments connect`.');
|
|
23
24
|
paymentsCommand
|
|
24
25
|
.command('connect')
|
|
25
26
|
.description('Start (or resume) Stripe onboarding for this app — prints a link to finish in your browser')
|
package/dist/commands/plan.js
CHANGED
|
@@ -31,7 +31,7 @@ function renderLimits(limits, indent = '') {
|
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
export const planCommand = new Command('plan')
|
|
34
|
-
.description('Show your
|
|
34
|
+
.description('Show your plan')
|
|
35
35
|
.option('--json', 'Output as JSON')
|
|
36
36
|
.action((opts) => run('Plan', async () => {
|
|
37
37
|
const [limitsRes, plansRes] = await Promise.all([
|
package/dist/commands/remove.js
CHANGED
|
@@ -7,7 +7,7 @@ import { run } from '../helpers/index.js';
|
|
|
7
7
|
import { confirm } from '../utils.js';
|
|
8
8
|
import { createProgressReporter, withSpinner } from '../progress.js';
|
|
9
9
|
export const removeCommand = new Command('remove')
|
|
10
|
-
.description('Remove
|
|
10
|
+
.description('Remove a kit')
|
|
11
11
|
.argument('<kit>', 'Kit key/directory under src/packages/ to remove')
|
|
12
12
|
.option('-y, --yes', 'Skip the confirmation prompt')
|
|
13
13
|
.option('--json', 'Output as JSON')
|
package/dist/commands/secrets.js
CHANGED
|
@@ -12,7 +12,8 @@ async function scopeQuery(opts) {
|
|
|
12
12
|
return `scope=project&app_guid=${config.projectGuid}`;
|
|
13
13
|
}
|
|
14
14
|
export const secretsCommand = new Command('secrets')
|
|
15
|
-
.description('
|
|
15
|
+
.description('Manage app secrets')
|
|
16
|
+
.addHelpText('after', '\nEncrypted API keys and tokens for your app, read in functions via secrets.get("NAME").');
|
|
16
17
|
secretsCommand
|
|
17
18
|
.command('list')
|
|
18
19
|
.alias('ls')
|
package/dist/commands/service.js
CHANGED
|
@@ -20,7 +20,8 @@ const SERVICES = [
|
|
|
20
20
|
{ name: 'location/geocode', method: 'POST', desc: 'Reverse geocode ({ lat, lon })' },
|
|
21
21
|
];
|
|
22
22
|
export const serviceCommand = new Command('service')
|
|
23
|
-
.description('Call
|
|
23
|
+
.description('Call an app service')
|
|
24
|
+
.addHelpText('after', '\nCall a Gipity app service: LLM, image, music, TTS, and more.');
|
|
24
25
|
serviceCommand
|
|
25
26
|
.command('list')
|
|
26
27
|
.description('List callable app services')
|
package/dist/commands/sync.js
CHANGED
|
@@ -3,7 +3,8 @@ import { sync } from '../sync.js';
|
|
|
3
3
|
import { createProgressReporter } from '../progress.js';
|
|
4
4
|
import { error as clrError, muted } from '../colors.js';
|
|
5
5
|
export const syncCommand = new Command('sync')
|
|
6
|
-
.description('Sync files
|
|
6
|
+
.description('Sync files')
|
|
7
|
+
.addHelpText('after', '\nAdd a .gipityignore at the project root to exclude paths (gitignore-style).')
|
|
7
8
|
.option('--plan', 'Print the plan without applying any changes')
|
|
8
9
|
.option('--force', 'Bypass the bulk-deletion guard')
|
|
9
10
|
.option('--prune', 'Remove files that exist on Gipity but not locally (applies the bulk deletes the guard defers)')
|
package/dist/commands/test.js
CHANGED
|
@@ -168,6 +168,12 @@ export const testCommand = new Command('test')
|
|
|
168
168
|
console.log(clrError(`No tests matched filter: ${filterPath}`));
|
|
169
169
|
process.exit(1);
|
|
170
170
|
}
|
|
171
|
+
if (!filterPath && data.total === 0 && data.results.length === 0) {
|
|
172
|
+
console.log(muted('No tests ran - this app has no tests/*.test.js files.'));
|
|
173
|
+
console.log(muted('gipity test runs server-function tests only; a frontend-only app (no functions/) has nothing here to run.'));
|
|
174
|
+
console.log(muted('Verify a frontend app by driving the live page: gipity page inspect <url> (or gipity page eval).'));
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
171
177
|
if (data.status === 'failed' && data.errorMessage) {
|
|
172
178
|
console.log(clrError(`Run failed: ${data.errorMessage}`));
|
|
173
179
|
console.log('');
|
package/dist/commands/text.js
CHANGED
|
@@ -140,7 +140,8 @@ const analyzeCommand = new Command('analyze')
|
|
|
140
140
|
// Parent namespace. One capability today (analyze); namespaced for a lean
|
|
141
141
|
// top-level surface and room for future text operations.
|
|
142
142
|
export const textCommand = new Command('text')
|
|
143
|
-
.description(
|
|
143
|
+
.description('Analyze text')
|
|
144
|
+
.addHelpText('after', `\nDeterministic char/word counts, frequency, and occurrences (${brand('text analyze')}). Local, no network.`)
|
|
144
145
|
.addCommand(analyzeCommand);
|
|
145
146
|
textCommand.action(() => {
|
|
146
147
|
textCommand.help();
|
package/dist/commands/token.js
CHANGED
|
@@ -3,7 +3,8 @@ import { get, post, del } from '../api.js';
|
|
|
3
3
|
import { bold, muted, success, warning } from '../colors.js';
|
|
4
4
|
import { run, printList } from '../helpers/index.js';
|
|
5
5
|
export const tokenCommand = new Command('token')
|
|
6
|
-
.description('Manage
|
|
6
|
+
.description('Manage API tokens')
|
|
7
|
+
.addHelpText('after', '\nLong-lived agent API tokens (gip_at_*) for headless agents and CI.');
|
|
7
8
|
const fmtDate = (d) => (d ? new Date(d).toLocaleDateString() : 'never');
|
|
8
9
|
tokenCommand
|
|
9
10
|
.command('create')
|
package/dist/config.js
CHANGED
|
@@ -134,8 +134,8 @@ export async function resolveProjectContext(opts) {
|
|
|
134
134
|
}
|
|
135
135
|
const agents = await get(`/projects/${match.short_guid}/agents`);
|
|
136
136
|
const accountSlug = await getAccountSlug();
|
|
137
|
-
console.error(dim(`→
|
|
138
|
-
console.error(
|
|
137
|
+
console.error(dim(`→ (project: ${match.slug} · no file sync)`));
|
|
138
|
+
console.error('');
|
|
139
139
|
return {
|
|
140
140
|
config: {
|
|
141
141
|
projectGuid: match.short_guid,
|
|
@@ -163,8 +163,8 @@ export async function resolveProjectContext(opts) {
|
|
|
163
163
|
console.error('Could not resolve your Home project - please contact support.');
|
|
164
164
|
process.exit(1);
|
|
165
165
|
}
|
|
166
|
-
console.error(dim(`→
|
|
167
|
-
console.error(
|
|
166
|
+
console.error(dim(`→ (project: ${res.data.projectName} · no file sync)`));
|
|
167
|
+
console.error('');
|
|
168
168
|
return {
|
|
169
169
|
config: {
|
|
170
170
|
projectGuid: res.data.projectGuid,
|
package/dist/helpers/output.js
CHANGED
|
@@ -3,16 +3,38 @@
|
|
|
3
3
|
* can close a frame even when a command ends via process.exit() and so skips
|
|
4
4
|
* the postAction hook. */
|
|
5
5
|
let frameOpen = false;
|
|
6
|
+
/** Count of newlines currently at the tail of everything written to stdout. Kept
|
|
7
|
+
* live by wrapping stdout.write so the frame can close to EXACTLY one trailing
|
|
8
|
+
* blank line instead of blindly appending one - the blind append doubled the
|
|
9
|
+
* blank whenever a command's own output already ended in a newline (e.g. an
|
|
10
|
+
* agent reply printed with a trailing '\n'). */
|
|
11
|
+
let trailingNewlines = 0;
|
|
12
|
+
function trackTrailing(chunk) {
|
|
13
|
+
const s = typeof chunk === 'string'
|
|
14
|
+
? chunk
|
|
15
|
+
: (chunk && typeof chunk.toString === 'function' ? String(chunk) : '');
|
|
16
|
+
if (!s.length)
|
|
17
|
+
return;
|
|
18
|
+
const run = /\n*$/.exec(s)?.[0].length ?? 0;
|
|
19
|
+
// A chunk that is all newlines extends the current run; otherwise it resets it
|
|
20
|
+
// to just this chunk's own trailing run.
|
|
21
|
+
trailingNewlines = run === s.length ? trailingNewlines + run : run;
|
|
22
|
+
}
|
|
6
23
|
/**
|
|
7
24
|
* Bracket every human (non-JSON) command's stdout with exactly one leading and
|
|
8
25
|
* one trailing blank line, so output is visually separated from the shell
|
|
9
26
|
* prompt and consistent across commands. JSON output is left untouched so it
|
|
10
27
|
* stays pipe/parse-clean.
|
|
11
28
|
*
|
|
29
|
+
* The trailing blank is added by TOP-UP, not blind append: one blank line means
|
|
30
|
+
* the stream ends in two newlines (the content's own line terminator plus one
|
|
31
|
+
* empty line), so we only write the newlines still missing. That keeps the
|
|
32
|
+
* boundary at exactly one blank whether the command ended its output with no
|
|
33
|
+
* newline, one, or an accidental extra.
|
|
34
|
+
*
|
|
12
35
|
* Centralizing the frame here is what lets individual commands stop printing
|
|
13
|
-
* their own leading/trailing blank lines
|
|
14
|
-
*
|
|
15
|
-
* hooks for the actual subcommand action too.
|
|
36
|
+
* their own leading/trailing blank lines. Call once on the root program;
|
|
37
|
+
* Commander runs these lifecycle hooks for the actual subcommand action too.
|
|
16
38
|
*/
|
|
17
39
|
export function installOutputFrame(program) {
|
|
18
40
|
// Only decorate a real terminal. When stdout is piped or redirected (a relay
|
|
@@ -20,6 +42,21 @@ export function installOutputFrame(program) {
|
|
|
20
42
|
// colors.ts uses for ANSI. This keeps headless `claude -p` stdout empty.
|
|
21
43
|
if (!process.stdout.isTTY)
|
|
22
44
|
return;
|
|
45
|
+
// Wrap stdout.write to keep `trailingNewlines` current for every byte the
|
|
46
|
+
// command emits (console.log, spinner, colors all route through here).
|
|
47
|
+
const origWrite = process.stdout.write.bind(process.stdout);
|
|
48
|
+
process.stdout.write = (chunk, ...rest) => {
|
|
49
|
+
trackTrailing(chunk);
|
|
50
|
+
return origWrite(chunk, ...rest);
|
|
51
|
+
};
|
|
52
|
+
const closeFrame = () => {
|
|
53
|
+
if (!frameOpen)
|
|
54
|
+
return;
|
|
55
|
+
frameOpen = false;
|
|
56
|
+
const need = Math.max(0, 2 - trailingNewlines);
|
|
57
|
+
if (need > 0)
|
|
58
|
+
origWrite('\n'.repeat(need));
|
|
59
|
+
};
|
|
23
60
|
program.hook('preAction', (_thisCommand, actionCommand) => {
|
|
24
61
|
const opts = actionCommand.optsWithGlobals
|
|
25
62
|
? actionCommand.optsWithGlobals()
|
|
@@ -29,18 +66,10 @@ export function installOutputFrame(program) {
|
|
|
29
66
|
process.stdout.write('\n');
|
|
30
67
|
frameOpen = true;
|
|
31
68
|
});
|
|
32
|
-
program.hook('postAction',
|
|
33
|
-
if (frameOpen) {
|
|
34
|
-
process.stdout.write('\n');
|
|
35
|
-
frameOpen = false;
|
|
36
|
-
}
|
|
37
|
-
});
|
|
69
|
+
program.hook('postAction', closeFrame);
|
|
38
70
|
// Commands that finish via process.exit() never reach postAction; close the
|
|
39
71
|
// frame from the exit handler instead (sync write, fires on every exit path).
|
|
40
|
-
process.on('exit',
|
|
41
|
-
if (frameOpen)
|
|
42
|
-
process.stdout.write('\n');
|
|
43
|
-
});
|
|
72
|
+
process.on('exit', closeFrame);
|
|
44
73
|
}
|
|
45
74
|
/**
|
|
46
75
|
* Print data as JSON or formatted text.
|
package/dist/knowledge.js
CHANGED
|
@@ -155,8 +155,10 @@ App services skills (load before calling \`/services/*\` endpoints):
|
|
|
155
155
|
|
|
156
156
|
App development skills:
|
|
157
157
|
- \`agent-deploy\` - headless auth via agent API tokens (GIPITY_TOKEN) for unattended deploys
|
|
158
|
+
- \`app-database\` - app Postgres database: migrations, the db helper, transactions, table permissions
|
|
158
159
|
- \`app-debugging\` - debug a deployed app: page inspect/eval, screenshots, function logs
|
|
159
160
|
- \`app-development\` - functions, database, and API
|
|
161
|
+
- \`app-testing\` - testing deployed app functions (ctx.fn.call/callAs, the isolated test DB)
|
|
160
162
|
- \`deploy\` - the deploy pipeline & gipity.yaml manifest
|
|
161
163
|
- \`jobs\` - long-running CPU + GPU compute jobs (Python / Node / bash)
|
|
162
164
|
- \`realtime-scheduled-app\` - recipe: realtime presence/messages + DB function + scheduled poster, end-to-end
|
package/dist/platform.js
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
|
-
import { execSync } from 'child_process';
|
|
1
|
+
import { execSync, spawn, spawnSync } from 'child_process';
|
|
2
2
|
/**
|
|
3
|
-
* Resolve a command to
|
|
3
|
+
* Resolve a command to a concrete path when possible.
|
|
4
4
|
*
|
|
5
5
|
* On Windows, npm-installed CLIs (npm, npx, gipity, claude) are `.cmd` batch
|
|
6
|
-
* shims, not `.exe`s.
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
6
|
+
* shims, not `.exe`s. A bare `npm` fails to spawn with ENOENT, so we resolve
|
|
7
|
+
* the real path (preferring a native `.exe`, falling back to the `.cmd` shim).
|
|
8
|
+
*
|
|
9
|
+
* NOTE: resolving to the `.cmd` path is NOT enough on its own. Since Node
|
|
10
|
+
* 18.20.2 / 20.12.2 (the CVE-2024-27980 fix), `spawn`/`spawnSync` REFUSE to
|
|
11
|
+
* launch a `.cmd`/`.bat` target unless `shell: true` - they throw `EINVAL`.
|
|
12
|
+
* So always launch a resolved command through `spawnCommand` /
|
|
13
|
+
* `spawnSyncCommand` below, which route batch shims through cmd.exe safely.
|
|
10
14
|
*/
|
|
11
15
|
export function resolveCommand(cmd) {
|
|
12
16
|
if (process.platform !== 'win32')
|
|
@@ -23,4 +27,47 @@ export function resolveCommand(cmd) {
|
|
|
23
27
|
return `${cmd}.cmd`;
|
|
24
28
|
}
|
|
25
29
|
}
|
|
30
|
+
/** True for a Windows batch shim that Node cannot spawn without a shell. */
|
|
31
|
+
export function isBatchShim(cmd) {
|
|
32
|
+
return process.platform === 'win32' && /\.(cmd|bat)$/i.test(cmd);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Build a cmd.exe invocation that launches a `.cmd`/`.bat` shim with its args
|
|
36
|
+
* passed through verbatim - the correct way to run a batch file post-CVE
|
|
37
|
+
* without `shell: true` mangling argument quoting.
|
|
38
|
+
*
|
|
39
|
+
* `cmd.exe /d /s /c "<line>"`: `/d` skips AutoRun, `/s` + the wrapping outer
|
|
40
|
+
* quotes make cmd strip exactly the first and last quote of the whole line
|
|
41
|
+
* (leaving each already-quoted token intact). We quote every token ourselves
|
|
42
|
+
* and set `windowsVerbatimArguments` so Node hands our line to cmd untouched.
|
|
43
|
+
*/
|
|
44
|
+
export function winBatchInvocation(cmd, args) {
|
|
45
|
+
const comspec = process.env.ComSpec || 'cmd.exe';
|
|
46
|
+
// Double-quote each token; escape any embedded double-quote as "" (cmd's
|
|
47
|
+
// in-quote escape). Our call sites pass flags/paths/guids with no cmd
|
|
48
|
+
// metacharacters, so this conservative quoting is sufficient and safe.
|
|
49
|
+
const quote = (t) => `"${String(t).replace(/"/g, '""')}"`;
|
|
50
|
+
const line = [cmd, ...args].map(quote).join(' ');
|
|
51
|
+
return { file: comspec, args: ['/d', '/s', '/c', `"${line}"`] };
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Drop-in `spawn` that survives Windows batch shims. When `cmd` is a `.cmd`/
|
|
55
|
+
* `.bat` on win32, it is launched via cmd.exe with verbatim arguments;
|
|
56
|
+
* otherwise this is a plain `spawn`. Use for anything from `resolveCommand`.
|
|
57
|
+
*/
|
|
58
|
+
export function spawnCommand(cmd, args = [], options = {}) {
|
|
59
|
+
if (isBatchShim(cmd)) {
|
|
60
|
+
const inv = winBatchInvocation(cmd, args);
|
|
61
|
+
return spawn(inv.file, inv.args, { ...options, windowsVerbatimArguments: true });
|
|
62
|
+
}
|
|
63
|
+
return spawn(cmd, [...args], options);
|
|
64
|
+
}
|
|
65
|
+
/** Drop-in `spawnSync` counterpart of {@link spawnCommand}. */
|
|
66
|
+
export function spawnSyncCommand(cmd, args = [], options = {}) {
|
|
67
|
+
if (isBatchShim(cmd)) {
|
|
68
|
+
const inv = winBatchInvocation(cmd, args);
|
|
69
|
+
return spawnSync(inv.file, inv.args, { ...options, windowsVerbatimArguments: true });
|
|
70
|
+
}
|
|
71
|
+
return spawnSync(cmd, [...args], options);
|
|
72
|
+
}
|
|
26
73
|
//# sourceMappingURL=platform.js.map
|
package/dist/relay/daemon.js
CHANGED
|
@@ -1,27 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* Gipity relay daemon - the `gipity-relay` long-running helper that backs
|
|
3
|
-
* `gipity relay run`. Runs two concurrent loops against the paired Gipity
|
|
4
|
-
* account using the device's bearer token:
|
|
5
|
-
*
|
|
6
|
-
* 1. Heartbeat every 60s → POST /remote-devices/heartbeat. Drives the
|
|
7
|
-
* web CLI's presence indicator.
|
|
8
|
-
* 2. Long-poll → GET /remote-devices/next. On a 200 claim, look up the
|
|
9
|
-
* dispatch's project in the local allowlist, spawn `gipity claude -p
|
|
10
|
-
* "<msg>"` in that project's cwd, wait for it to exit, POST ack.
|
|
11
|
-
*
|
|
12
|
-
* The conversation stream (prompts, tool calls, assistant output) flows
|
|
13
|
-
* back to the web CLI *automatically* via the capture hooks installed in
|
|
14
|
-
* `.claude/settings.json` - the daemon itself doesn't forward content.
|
|
15
|
-
*
|
|
16
|
-
* Graceful exit:
|
|
17
|
-
* - SIGINT / SIGTERM → stop both loops, wait for in-flight child, exit 0.
|
|
18
|
-
* - 401 from heartbeat or /next → device was revoked; exit 0.
|
|
19
|
-
* - Any other backend error → log and retry with exponential backoff.
|
|
20
|
-
*
|
|
21
|
-
* See docs/feature-backlog/gipity-relay-phases.md (Phase A Step 7).
|
|
22
|
-
*/
|
|
23
|
-
import { spawn } from 'child_process';
|
|
24
|
-
import { resolveCommand } from '../platform.js';
|
|
1
|
+
import { resolveCommand, spawnCommand } from '../platform.js';
|
|
25
2
|
import { appendFileSync, mkdirSync, existsSync, readFileSync, writeFileSync, chmodSync, closeSync, openSync, unlinkSync } from 'fs';
|
|
26
3
|
import { stat, readFile } from 'fs/promises';
|
|
27
4
|
import { createInterface } from 'readline';
|
|
@@ -528,14 +505,15 @@ function isSafeSessionId(s) {
|
|
|
528
505
|
return /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(s);
|
|
529
506
|
}
|
|
530
507
|
/** Resolve Claude Code's on-disk transcript path for measuring resume
|
|
531
|
-
* size. Claude encodes the project cwd into a slug by replacing
|
|
532
|
-
*
|
|
533
|
-
*
|
|
508
|
+
* size. Claude encodes the project cwd into a slug by replacing every
|
|
509
|
+
* non-alphanumeric char with `-` (so `/`, and on Windows `\` and `:`, all
|
|
510
|
+
* collapse). We only read this file cosmetically (to show "resume 5 KB" in
|
|
511
|
+
* the Invoking marker); actual capture is via stream-json. Returns null for
|
|
534
512
|
* a sessionId that fails the safety check. */
|
|
535
513
|
function transcriptPathFor(cwd, sessionId) {
|
|
536
514
|
if (!isSafeSessionId(sessionId))
|
|
537
515
|
return null;
|
|
538
|
-
const slug = cwd.replace(
|
|
516
|
+
const slug = cwd.replace(/[^a-zA-Z0-9]/g, '-');
|
|
539
517
|
return join(homedir(), '.claude', 'projects', slug, `${sessionId}.jsonl`);
|
|
540
518
|
}
|
|
541
519
|
/** `18.4s` when under a minute, `3:12.2` above. */
|
|
@@ -867,7 +845,7 @@ async function spawnSync(cwd, timeoutMs) {
|
|
|
867
845
|
// verbatim (it may be a full path); only the default name is resolved.
|
|
868
846
|
const cmd = process.env.GIPITY_RELAY_CLAUDE_CMD || resolveCommand('gipity');
|
|
869
847
|
return new Promise((resolve, reject) => {
|
|
870
|
-
const child =
|
|
848
|
+
const child = spawnCommand(cmd, ['sync', '--json'], {
|
|
871
849
|
cwd,
|
|
872
850
|
env: process.env,
|
|
873
851
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
@@ -921,7 +899,7 @@ export async function spawnGipityClaude(args, cwd, d) {
|
|
|
921
899
|
const fullArgs = [...args, '--output-format', 'stream-json', '--verbose'];
|
|
922
900
|
const env = { ...process.env, GIPITY_CONVERSATION_GUID: d.conversation_guid };
|
|
923
901
|
return new Promise((resolve, reject) => {
|
|
924
|
-
const child =
|
|
902
|
+
const child = spawnCommand(cmd, fullArgs, { cwd, env, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
925
903
|
// `exited` fires when the child fully unwinds (exit event). Callers
|
|
926
904
|
// like `killRunningForConv` await this before spawning a replacement
|
|
927
905
|
// so the outgoing child has a chance to post its cancelled marker
|
package/dist/relay/installers.js
CHANGED
|
@@ -126,6 +126,14 @@ WantedBy=default.target
|
|
|
126
126
|
function windowsTaskPlan(cliPath) {
|
|
127
127
|
const taskName = 'GipityRelay';
|
|
128
128
|
const path = join(homedir(), 'AppData', 'Local', 'Gipity', 'relay-task.xml');
|
|
129
|
+
// Launch through node.exe explicitly. Task Scheduler's <Command> runs the
|
|
130
|
+
// target via CreateProcess, and a `.js` cliPath (what resolveCliPath yields)
|
|
131
|
+
// would resolve to its file association - Windows Script Host - which chokes
|
|
132
|
+
// on the shebang with "Invalid character" (800A03F6). Passing the entry as a
|
|
133
|
+
// node argument runs it under Node like every other platform.
|
|
134
|
+
const runsViaNode = /\.[cm]?js$/i.test(cliPath);
|
|
135
|
+
const command = runsViaNode ? process.execPath : cliPath;
|
|
136
|
+
const argLine = runsViaNode ? `"${cliPath}" relay run` : 'relay run';
|
|
129
137
|
const content = `<?xml version="1.0" encoding="UTF-16"?>
|
|
130
138
|
<Task version="1.4" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
|
|
131
139
|
<RegistrationInfo>
|
|
@@ -151,8 +159,8 @@ function windowsTaskPlan(cliPath) {
|
|
|
151
159
|
</Settings>
|
|
152
160
|
<Actions>
|
|
153
161
|
<Exec>
|
|
154
|
-
<Command>${
|
|
155
|
-
<Arguments
|
|
162
|
+
<Command>${command}</Command>
|
|
163
|
+
<Arguments>${argLine}</Arguments>
|
|
156
164
|
</Exec>
|
|
157
165
|
</Actions>
|
|
158
166
|
</Task>
|
package/dist/setup.js
CHANGED
|
@@ -5,7 +5,7 @@ import { resolve, join, dirname } from 'path';
|
|
|
5
5
|
import { homedir } from 'os';
|
|
6
6
|
import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'fs';
|
|
7
7
|
import { spawnSync } from 'child_process';
|
|
8
|
-
import { resolveCommand } from './platform.js';
|
|
8
|
+
import { resolveCommand, spawnSyncCommand } from './platform.js';
|
|
9
9
|
import { SKILLS_CONTENT, BUILD_VS_NON_BUILD_RULE, DEFINITION_OF_DONE } from './knowledge.js';
|
|
10
10
|
export { SKILLS_CONTENT };
|
|
11
11
|
/** Canonical list of workstation artifacts that are NOT part of the project.
|
|
@@ -137,7 +137,7 @@ export const GIPITY_MARKETPLACE_REPO = 'GipityAI/claude-plugin';
|
|
|
137
137
|
// an installed plugin when the marketplace advances - only an explicit
|
|
138
138
|
// `plugin install`/`update` does - so this constant is how a CLI upgrade tells
|
|
139
139
|
// ensureGipityPluginInstalled() to refresh a stale user-scope install.
|
|
140
|
-
export const GIPITY_PLUGIN_VERSION = '0.4.
|
|
140
|
+
export const GIPITY_PLUGIN_VERSION = '0.4.1';
|
|
141
141
|
/** True for hook commands the CLI itself wrote into settings.json in past
|
|
142
142
|
* versions. Matched by signature so migration strips exactly our own
|
|
143
143
|
* entries and never touches user-authored hooks. */
|
|
@@ -288,11 +288,11 @@ export function ensureGipityPluginInstalled() {
|
|
|
288
288
|
// without an explicit path, so resolve it (otherwise the install silently
|
|
289
289
|
// ENOENTs and the plugin's hooks never land at user scope).
|
|
290
290
|
const claudeCmd = resolveCommand('claude');
|
|
291
|
-
|
|
291
|
+
spawnSyncCommand(claudeCmd, ['plugin', 'marketplace', 'update', GIPITY_MARKETPLACE_NAME], {
|
|
292
292
|
stdio: 'ignore',
|
|
293
293
|
timeout: 120_000,
|
|
294
294
|
});
|
|
295
|
-
|
|
295
|
+
spawnSyncCommand(claudeCmd, ['plugin', 'install', GIPITY_PLUGIN_ID, '--scope', 'user'], {
|
|
296
296
|
stdio: 'ignore',
|
|
297
297
|
timeout: 120_000,
|
|
298
298
|
});
|