gipity 1.0.392 → 1.0.394

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.
@@ -68,12 +68,103 @@ agentCommand
68
68
  else if (field === 'temp' || field === 'temperature')
69
69
  body.temperature = parseFloat(value);
70
70
  else {
71
- console.error(clrError(`Unknown field: ${field}. Use: model, temp`));
71
+ console.error(clrError(`Unknown field: ${field}. Use: model, temp (for soul/goal use \`gipity agent soul|goal\`)`));
72
72
  process.exit(1);
73
73
  }
74
74
  await put(`/agents/${config.agentGuid}`, body);
75
75
  printResult(`Set ${field} = ${value}`, opts, { success: true, field, value });
76
76
  }));
77
+ /** The active agent's guid, or a clear error - the brain commands all need one. */
78
+ function requireAgentGuid() {
79
+ const config = requireConfig();
80
+ if (!config.agentGuid) {
81
+ console.error(clrError('No active agent. Switch to one with: gipity agent <name>'));
82
+ process.exit(1);
83
+ }
84
+ return config.agentGuid;
85
+ }
86
+ // --- Brain: soul / goal / rules / learn ---
87
+ // These hit the account-scoped /account/agents surface (the same dual-auth,
88
+ // app-callable routes a deployed app uses), so the CLI, the web terminal, and an
89
+ // app all drive the agent's brain through one set of endpoints. No more
90
+ // hand-rolled `curl -X PUT a.gipity.ai/agents/:guid/soul` with a scraped token.
91
+ agentCommand
92
+ .command('soul [text...]')
93
+ .description("Show the current agent's soul, or set it (its voice/personality)")
94
+ .option('--json', 'Output as JSON')
95
+ .action((text, opts) => run('Soul', async () => {
96
+ const guid = requireAgentGuid();
97
+ if (text && text.length) {
98
+ const content = text.join(' ');
99
+ const res = await put(`/account/agents/${guid}/soul`, { content });
100
+ printResult('Soul updated.', opts, res.data);
101
+ }
102
+ else {
103
+ const res = await get(`/account/agents/${guid}/soul`);
104
+ printResult(res.data.content || '(no soul set)', opts, res.data);
105
+ }
106
+ }));
107
+ agentCommand
108
+ .command('goal [text...]')
109
+ .description("Show the current agent's goal, or set it")
110
+ .option('--clear', 'Clear the goal (back to a plain assistant)')
111
+ .option('--json', 'Output as JSON')
112
+ .action((text, opts) => run('Goal', async () => {
113
+ const guid = requireAgentGuid();
114
+ if (opts.clear) {
115
+ const res = await put(`/account/agents/${guid}/goal`, { goal: null });
116
+ printResult('Goal cleared.', opts, res.data);
117
+ }
118
+ else if (text && text.length) {
119
+ const goal = text.join(' ');
120
+ const res = await put(`/account/agents/${guid}/goal`, { goal });
121
+ printResult('Goal updated.', opts, res.data);
122
+ }
123
+ else {
124
+ const res = await get(`/account/agents/${guid}/goal`);
125
+ printResult(res.data.goal || '(no goal set)', opts, res.data);
126
+ }
127
+ }));
128
+ const rulesCommand = agentCommand
129
+ .command('rules')
130
+ .description("Show the agent's rules playbook (manual + learned)")
131
+ .option('--json', 'Output as JSON')
132
+ .action((opts) => run('Rules', async () => {
133
+ const guid = requireAgentGuid();
134
+ const res = await get(`/account/agents/${guid}/rules`);
135
+ printList(res.data, opts, 'No rules yet.', r => `[${r.source}] ${r.short_guid} ${r.text}`);
136
+ }));
137
+ rulesCommand
138
+ .command('add <text...>')
139
+ .description('Add a manual rule')
140
+ .option('--json', 'Output as JSON')
141
+ .action((text, opts) => run('Add', async () => {
142
+ const guid = requireAgentGuid();
143
+ const res = await post(`/account/agents/${guid}/rules`, { text: text.join(' ') });
144
+ printResult(`Added rule ${res.data[0].short_guid}.`, opts, res.data[0]);
145
+ }));
146
+ rulesCommand
147
+ .command('rm <rule-guid>')
148
+ .alias('delete')
149
+ .description('Deactivate a rule by its guid')
150
+ .option('--json', 'Output as JSON')
151
+ .action((ruleGuid, opts) => run('Remove', async () => {
152
+ const guid = requireAgentGuid();
153
+ await del(`/account/agents/${guid}/rules/${ruleGuid}`);
154
+ printResult(`Removed rule ${ruleGuid}.`, opts, { removed: ruleGuid });
155
+ }));
156
+ agentCommand
157
+ .command('learn')
158
+ .description("Teach the agent from one correction (distills a durable learned rule)")
159
+ .requiredOption('--original <text>', 'What the agent originally produced')
160
+ .requiredOption('--comment <text>', "Your correction / why it was wrong")
161
+ .option('--json', 'Output as JSON')
162
+ .action((opts) => run('Learn', async () => {
163
+ const guid = requireAgentGuid();
164
+ const res = await post(`/account/agents/${guid}/learn`, { original: opts.original, comment: opts.comment });
165
+ const d = res.data;
166
+ printResult(d.saved ? `Learned: ${d.rule.text}` : `No rule saved (${d.reason || 'too idiosyncratic to generalize'}).`, opts, d);
167
+ }));
77
168
  agentCommand
78
169
  .command('rename <new-name>')
79
170
  .description('Rename the current agent')
@@ -2,7 +2,7 @@ import { Command } from 'commander';
2
2
  import { get, post, sendMessage } from '../api.js';
3
3
  import { requireConfig } from '../config.js';
4
4
  import { error as clrError, success } from '../colors.js';
5
- import { run, printList } from '../helpers/index.js';
5
+ import { run, printList, emitField } from '../helpers/index.js';
6
6
  import { confirm } from '../utils.js';
7
7
  export const dbCommand = new Command('db')
8
8
  .description('Manage databases');
@@ -10,6 +10,7 @@ dbCommand
10
10
  .command('query <sql>')
11
11
  .description('Run SQL')
12
12
  .option('--database <name>', 'Database name')
13
+ .option('--field <path>', 'Print only this field of the result (dot path, e.g. rows.0.status)')
13
14
  .option('--json', 'Output as JSON')
14
15
  .action((sql, opts) => run('Query', async () => {
15
16
  const config = requireConfig();
@@ -24,7 +25,10 @@ dbCommand
24
25
  dbName = listRes.data[0].friendlyName;
25
26
  }
26
27
  const res = await post(`/projects/${config.projectGuid}/db/query`, { sql, database: dbName });
27
- if (opts.json) {
28
+ if (opts.field) {
29
+ emitField(res.data, opts.field);
30
+ }
31
+ else if (opts.json) {
28
32
  console.log(JSON.stringify(res.data));
29
33
  }
30
34
  else {
@@ -2,7 +2,7 @@ import { Command } from 'commander';
2
2
  import { get, post, del } from '../api.js';
3
3
  import { requireConfig } from '../config.js';
4
4
  import { error as clrError, bold, muted, success } from '../colors.js';
5
- import { run, printList } from '../helpers/index.js';
5
+ import { run, printList, emitField } from '../helpers/index.js';
6
6
  import { confirm } from '../utils.js';
7
7
  export const fnCommand = new Command('fn')
8
8
  .description('Manage functions');
@@ -38,12 +38,17 @@ fnCommand
38
38
  .command('call <name> [body]')
39
39
  .description('Call a function')
40
40
  .option('--data <json>', 'JSON request body')
41
+ .option('--field <path>', 'Print only this field of the result (dot path, e.g. items.0.short_guid)')
41
42
  .option('--json', 'Output as JSON')
42
43
  .action((name, bodyArg, opts) => run('Call', async () => {
43
44
  const config = requireConfig();
44
45
  const raw = bodyArg || opts.data || '{}';
45
46
  const body = JSON.parse(raw);
46
47
  const res = await post(`/api/${config.projectGuid}/fn/${encodeURIComponent(name)}`, body);
48
+ if (opts.field) {
49
+ emitField(res.data, opts.field);
50
+ return;
51
+ }
47
52
  console.log(opts.json ? JSON.stringify(res.data) : JSON.stringify(res.data, null, 2));
48
53
  }));
49
54
  fnCommand
@@ -1,5 +1,5 @@
1
1
  import { readFileSync } from 'node:fs';
2
- import { Command } from 'commander';
2
+ import { Command, Option } from 'commander';
3
3
  import { post, get, ApiError } from '../api.js';
4
4
  import { brand, bold, muted, warning } from '../colors.js';
5
5
  import { run } from '../helpers/index.js';
@@ -122,6 +122,13 @@ export function evalExecTimeoutMessage(result) {
122
122
  `and cannot extend it. Split a long interactive check into several shorter 'page eval' calls (e.g. ` +
123
123
  `one per state to verify), keeping each body's in-page waits well under ${EVAL_EXEC_BUDGET_MS / 1000}s.`);
124
124
  }
125
+ // Agents instinctively reach for a flag to pass the script (`--js`, `--script`,
126
+ // `--code`, …); the JS is actually the positional <expr> (or --file for a saved
127
+ // script). Without these, commander answers `--js` with "did you mean --json?" —
128
+ // a trap, since --json is a real flag that changes output but still leaves the
129
+ // script unset, sending the agent in a loop. Capture the common guesses as
130
+ // hidden decoy options so the action can redirect to the positional arg exactly.
131
+ const JS_DECOY_FLAGS = ['--js', '--javascript', '--script', '--code', '--expr', '--eval', '--exec'];
125
132
  // The long-tail escape hatch alongside `page inspect`'s fixed bundle: when the
126
133
  // curated metrics don't cover what you need (computed styles, element rects,
127
134
  // visibility, z-index stacks), eval an expression in page context and get the
@@ -143,6 +150,13 @@ export const pageEvalCommand = new Command('eval')
143
150
  .option('--wait-timeout <ms>', 'Max ms to wait for --wait-for before giving up', '5000')
144
151
  .option('--json', 'Output as JSON')
145
152
  .action((url, exprArg, opts) => run('Page eval', async () => {
153
+ // A JS-intent flag guess (captured as a hidden decoy below): redirect to the
154
+ // positional <expr> precisely, before the inline/--file shape checks fire.
155
+ const decoy = JS_DECOY_FLAGS.find((f) => opts[f.slice(2)] !== undefined);
156
+ if (decoy) {
157
+ pageEvalCommand.error(`error: ${decoy} is not a flag — pass the JavaScript as the positional <expr> argument ` +
158
+ `(or --file <path> for a saved script), e.g. gipity page eval "<url>" 'document.title'`);
159
+ }
146
160
  // Arg-shape errors go through commander's error() so the enableHelpAfterError
147
161
  // hook renders this command's help inline with the one-line error LAST
148
162
  // (survives `| tail`), same as commander-detected errors like a missing url.
@@ -225,6 +239,11 @@ export const pageEvalCommand = new Command('eval')
225
239
  }
226
240
  }
227
241
  }));
242
+ // Register the JS-intent flag guesses as hidden decoys (take a value so they
243
+ // swallow the script the agent passed) — the action turns any of them into the
244
+ // precise "JS is the positional arg" redirect above.
245
+ for (const f of JS_DECOY_FLAGS)
246
+ pageEvalCommand.addOption(new Option(`${f} <value>`).hideHelp());
228
247
  // Each `page eval` call runs to completion before the next starts, so two evals
229
248
  // fired back-to-back never coexist in time - they CANNOT test whether two live
230
249
  // clients see each other (presence, shared state). For that, use the genuinely-
@@ -62,6 +62,40 @@ export const pageInspectCommand = new Command('inspect')
62
62
  };
63
63
  const res = await post(`/tools/browser/inspect`, inspectBody);
64
64
  const b = res.data;
65
+ // ── Strip the platform's own instrumentation noise first ──
66
+ // Every deployed page loads Gipity's injected analytics SDK, which POSTs to
67
+ // Gipity's traffic/error log endpoints (`/api/<guid>/log/traffic|error`).
68
+ // Those are platform infrastructure, not the app's resources, so when one
69
+ // fails it surfaces as a failed resource on the Gipity host PLUS a generic,
70
+ // URL-less "Failed to load resource" console error — identical noise on
71
+ // essentially every deployed app. Drop both so an agent inspecting the app
72
+ // it just built sees only its own code's resources, not the platform's.
73
+ const isPlatformLog = (entry) => {
74
+ const urlPart = entry.replace(/\s*\([^)]*\)\s*$/, '');
75
+ try {
76
+ const u = new URL(urlPart);
77
+ return /(^|\.)gipity\.ai$/.test(u.hostname) && /\/log\/(traffic|error)$/.test(u.pathname);
78
+ }
79
+ catch {
80
+ return false;
81
+ }
82
+ };
83
+ const platformFailures = (b.failedResources || []).filter(isPlatformLog);
84
+ b.failedResources = (b.failedResources || []).filter((r) => !isPlatformLog(r));
85
+ // Each failed platform POST also emits exactly one generic, URL-less
86
+ // "Failed to load resource" console error. Drop one per platform failure —
87
+ // the text is identical, so removing by count is exact and any genuine app
88
+ // 404 keeps its own (indistinguishable) line.
89
+ let platformConsoleToDrop = platformFailures.length;
90
+ if (platformConsoleToDrop > 0) {
91
+ b.console = (b.console || []).filter((l) => {
92
+ if (platformConsoleToDrop > 0 && /^error:\s*Failed to load resource:/i.test(l)) {
93
+ platformConsoleToDrop--;
94
+ return false;
95
+ }
96
+ return true;
97
+ });
98
+ }
65
99
  // Pull message-less cross-origin "Script error." lines out first. They carry
66
100
  // no source/stack, so they're never actionable as app-code defects, and on a
67
101
  // Gipity-deployed page the platform's own injected SDK is itself a
@@ -100,6 +100,7 @@ export const pageScreenshotCommand = new Command('screenshot')
100
100
  // so the `?? opts.wait` merge below would never see the --wait alias. Default
101
101
  // is applied in the merge instead.
102
102
  .option('--post-load-delay <ms>', 'Delay after DOMContentLoaded before capture, in ms (default: 1000)')
103
+ .option('--action <js>', 'Run JS in the page before capturing — e.g. click a button to enter a state ("document.getElementById(\'play\').click()"). Runs after the post-load delay, then settles again before the shot.')
103
104
  .option('--full', 'Capture the full scrollable page (default: viewport only)')
104
105
  .option('-o, --output <file>', 'Output path (single viewport only; default .gipity/screenshots/ss-<host>-<timestamp>.png)')
105
106
  .option('--device <names>', `Viewport preset(s): ${Object.keys(DEVICE_PRESETS).join(', ')} (comma-separated or repeat flag)`, appendOption, [])
@@ -140,6 +141,7 @@ export const pageScreenshotCommand = new Command('screenshot')
140
141
  reloadBetween: opts.reloadBetween !== false,
141
142
  ...(userSpecifiedViewports ? { viewports: customViewports } : {}),
142
143
  ...(opts.fakeMedia ? { fakeMedia: true } : {}),
144
+ ...(opts.action ? { action: opts.action } : {}),
143
145
  };
144
146
  const entries = await postForTarEntries('/tools/browser/screenshot', body);
145
147
  const metaEntry = entries.find((e) => e.name === 'meta.json');
@@ -231,23 +233,26 @@ export const pageScreenshotCommand = new Command('screenshot')
231
233
  console.log(`${label('Screenshot file')} ${success(savedFiles[i])}`);
232
234
  }
233
235
  }));
234
- // `screenshot` captures the page AS IT LOADS there is no flag to scroll, click,
235
- // run a script, or wait for a selector before capture (agents reach for --eval/
236
- // --script/--scroll/--selector and get an unknown-option detour). State this
237
- // limitation and the two supported alternatives right here, so the help (rendered
238
- // on any bad flag, and this 'after' block survives `| tail`/`| grep`) ends the
239
- // hunt in one shot instead of sending the agent grepping `--help` for
240
- // scroll/script/before/action. The real per-element capture lives server-side and
241
- // isn't wired yet — until then, --full + crop or `page eval` cover the need.
236
+ // `screenshot` captures the page AFTER load + settle (+ optional --action). It does
237
+ // NOT scroll or wait for a selector before capture (agents reach for --scroll/
238
+ // --selector and get an unknown-option detour). State the supported levers right
239
+ // here, so the help (rendered on any bad flag, and this 'after' block survives
240
+ // `| tail`/`| grep`) ends the hunt in one shot. --action covers "click, then shoot";
241
+ // --full + crop covers off-screen regions; `page eval` reads data without a picture.
242
242
  pageScreenshotCommand.addHelpText('after', `
243
243
  Examples:
244
244
  gipity page screenshot "https://dev.gipity.ai/me/app/"
245
245
  gipity page screenshot "https://dev.gipity.ai/me/app/" --full # whole scrollable page
246
246
  gipity page screenshot "https://dev.gipity.ai/me/app/" --device mobile,desktop
247
+ gipity page screenshot "https://dev.gipity.ai/me/app/" \\
248
+ --action "document.getElementById('play').click()" # capture an in-game frame
247
249
 
248
- Capturing a specific state (a slide, an off-screen element, a post-scroll view)?
249
- screenshot captures the page as it loads — it does NOT scroll, click, wait for a
250
- selector, or run a script before capture. To get the part you want:
250
+ Capturing a state that needs an interaction (start a game, open a menu, dismiss a modal)?
251
+ Use --action to run JS in the page before the shot — it fires after the post-load
252
+ delay, then settles again so the result has painted. Do NOT hand-roll a 'page eval'
253
+ that returns a base64 image: the eval result is capped (~16KB) and truncates the PNG.
254
+
255
+ Capturing an off-screen region or reading element data?
251
256
  • --full captures the ENTIRE scrollable page (then crop to the region).
252
257
  • 'gipity page eval <url> "<expr>"' reads any (even off-screen) element's
253
258
  data/state/rect without a picture — e.g. read the chart's bar values
@@ -150,6 +150,17 @@ GCC/Rust).
150
150
  }
151
151
  const timeout = parseInt(opts.timeout, 10);
152
152
  const cwd = resolveRelativeCwd();
153
+ // Push local working-tree changes up before executing. The sandbox mirrors
154
+ // the *server* (VFS), not the local cwd, so any input staged outside Claude's
155
+ // Write/Edit auto-push hook - a Bash `cp`/`ffmpeg`/redirect, or any external
156
+ // process - would otherwise be invisible to the run and the first invocation
157
+ // would silently miss its inputs. Syncing first makes the auto-mirror reflect
158
+ // local state regardless of how files got there ("no manual copy needed").
159
+ // Bidirectional + CAS, so it's a cheap manifest check when nothing changed.
160
+ // Symmetric with the post-run pull below. Skip in one-off mode (no project).
161
+ if (getConfigPath()) {
162
+ await sync({ interactive: false });
163
+ }
153
164
  const res = await post(`/projects/${config.projectGuid}/sandbox/execute`, {
154
165
  code: source,
155
166
  language,
@@ -2,6 +2,6 @@
2
2
  * CLI helpers - shared patterns across all commands.
3
3
  */
4
4
  export { run } from './command.js';
5
- export { printOutput, printList, printResult } from './output.js';
5
+ export { printOutput, printList, printResult, pluckField, emitField } from './output.js';
6
6
  export { syncBeforeAction } from './sync.js';
7
7
  //# sourceMappingURL=index.js.map
@@ -63,6 +63,29 @@ export function printResult(text, opts, jsonData) {
63
63
  }
64
64
  console.log(text);
65
65
  }
66
+ /**
67
+ * Pluck a nested value out of a result by dot-path. Numeric segments index into
68
+ * arrays, so `items.0.short_guid` reaches the first item's guid. Returns
69
+ * `undefined` if any segment along the way is missing. This is what lets
70
+ * `--field` replace the `... | node -e "JSON.parse(...)"` extraction dance.
71
+ */
72
+ export function pluckField(data, path) {
73
+ return path.split('.').reduce((acc, key) => (acc == null ? undefined : acc[key]), data);
74
+ }
75
+ /**
76
+ * Emit a single plucked field for `--field`. Scalars print raw (no quotes) so
77
+ * the output drops straight into `$(...)` / pipes; objects/arrays print as
78
+ * compact JSON. A missing path is an error (exit 1) so scripts fail loudly
79
+ * instead of silently consuming an empty string.
80
+ */
81
+ export function emitField(data, path) {
82
+ const value = pluckField(data, path);
83
+ if (value === undefined) {
84
+ console.error(`Field not found: ${path}`);
85
+ process.exit(1);
86
+ }
87
+ console.log(typeof value === 'object' && value !== null ? JSON.stringify(value) : String(value));
88
+ }
66
89
  /**
67
90
  * Print a list with JSON mode, empty state, and per-item formatting.
68
91
  * Replaces the most common output pattern across all commands.
package/dist/knowledge.js CHANGED
@@ -20,6 +20,7 @@ Templates:
20
20
  - \`3d-world\` - Multiplayer world, 3D sandbox, shooter, exploration, virtual showroom (Three.js + Rapier + Colyseus)
21
21
  - \`api\` - Backend service, webhook, data pipeline, chatbot, cron job - no frontend
22
22
  - \`karaoke-captions\` - Forced-alignment app - karaoke captions, subtitle timing, language learning, dubbing alignment
23
+ - \`outreach-agent\` - AI outreach / drip-email funnel - reach a list of people with personalized, human-approved emails that auto-send on a schedule and a self-improving agent that learns from your edits
23
24
  When unsure, default to \`web-simple\`. After adding the template, edit the generated files, then \`gipity deploy dev\`.
24
25
  Only skip this on a build request if the user explicitly says not to.
25
26
 
@@ -36,7 +37,8 @@ Kits are reusable building blocks added to an existing app, not whole templates
36
37
  - \`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
38
  - \`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
39
  - \`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.`;
40
+ - \`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.
41
+ - \`gipity add contacts\` - Source-agnostic contact data layer for lead-gen/CRM apps: import people from LinkedIn CSV + Gmail + pasted lists, resolve duplicates into one person while keeping EVERY value from every source with provenance (multi-valued attributes, never overwrites). Exact email/URL auto-merge; fuzzy name+company goes to a human merge-review queue (reversible). Re-imports detect job changes and emit signals. User-definable tags, full-text search, and a transactional event spine. Ships backend functions + migrations. Needs a database (web-fullstack/api template).`;
40
42
  export const SKILLS_CONTENT = `# Gipity Integration
41
43
 
42
44
  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.
@@ -45,7 +47,7 @@ Prefer the cheapest option that works - CLI and sandbox are instant and free, ap
45
47
 
46
48
  1. CLI commands (fast, no agent overhead). The \`gipity\` CLI covers add, deploy, db, fn, logs, browser, sync, memory, skill, and more. All commands support \`--json\`.
47
49
  2. Cloud sandbox via \`gipity sandbox run\` - Docker container with pre-installed tools for media (ffmpeg, ImageMagick, sox), documents (pandoc, LibreOffice), and data (pandas, matplotlib, sqlite3). Run \`gipity skill read sandbox-tools\` for the full toolkit. No network from inside the sandbox - fetch what you need before sending it in.
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.
50
+ 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.
49
51
  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.
50
52
 
51
53
  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.
@@ -78,21 +80,32 @@ The full "when to add a template" rule and the definition of done are spelled ou
78
80
 
79
81
  Build loop: \`gipity add\` → edit files → \`gipity deploy dev\` → \`gipity page inspect <url>\` → fix any errors → repeat until the definition of done is met.
80
82
 
81
- \`add\` writes real files to disk Read a scaffolded file before your first Write/Edit to it, or the call fails \`"File has not been read yet"\`. Don't rewrite from memory of the template.
83
+ \`add\` writes real files to disk - Read a scaffolded file before your first Write/Edit to it, or the call fails \`"File has not been read yet"\`. Don't rewrite from memory of the template.
82
84
 
83
85
  Make your file changes and verify they landed, then run \`gipity deploy dev\` once. \`0 uploaded, N unchanged\` means nothing changed on disk - fix the files, don't re-run deploy or probe the environment.
84
86
 
85
87
  Before telling the user the app is online, verify the source tree is consistent: no files named like \`* (conflict from *)*\`, and every package directory has its expected canonical entry file. If a conflict artifact exists, resolve it (keep one copy), re-deploy, and re-inspect before reporting done.
86
88
 
89
+ ## Work on an existing project that isn't local yet
90
+
91
+ If you're pointed at a project that already exists on Gipity but has no local copy - e.g. the user gives a live URL \`https://dev.gipity.ai/<account>/<slug>/\` (or \`app.gipity.ai\` for prod) and you need its files to edit them - the last path segment is the project **slug**. Pull it down by adopting it into a directory named for the slug:
92
+
93
+ \`\`\`
94
+ mkdir -p ~/GipityProjects/<slug> && cd ~/GipityProjects/<slug> && gipity init <slug>
95
+ \`\`\`
96
+
97
+ \`init\` matches the existing remote project by slug, links this directory to it, and syncs its files down (you'll see \`Found existing project ...\` and \`Synced N changes\`). There's no separate \`clone\`/\`pull\` - \`init\` against a matching slug *is* the pull. After it finishes, the files are in cwd; edit and \`gipity deploy dev\` as usual. (Already linked to a different project in this dir? Switch and pull instead: \`gipity project <slug>\` then \`gipity sync\`. List your projects with \`gipity project --json\`.)
98
+
87
99
  ## CLI quick reference
88
100
 
89
101
  Key commands: \`gipity add <template|kit>\`, \`gipity deploy dev\`, \`gipity sandbox run\`, \`gipity page inspect <url>\`, \`gipity page screenshot <url>\`, \`gipity db query "SQL"\`, \`gipity fn call <name>\`, \`gipity logs fn <name>\`, \`gipity skill read <name>\`.
102
+ Pull an existing remote project local (given its URL/slug): \`mkdir -p ~/GipityProjects/<slug> && cd ~/GipityProjects/<slug> && gipity init <slug>\` (adopts the matching project and syncs files down - this is the "clone").
90
103
  For deterministic text questions (letter/word counts, substring occurrences, nth word/char, anagrams), use \`gipity text analyze "<text>"\` - local and instant, no sandbox or LLM needed.
91
104
  Run \`gipity --help\` for the full list. Use \`--help\` on any command for details.
92
105
 
93
- Function return shape: \`gipity fn call\`, the in-test \`ctx.fn.call\`/\`callAs\`, and the client \`Gipity.fn\` all return your function's value **unwrapped** read/assert \`result.field\`. Only raw HTTP/\`curl\` wraps it as \`{ data: ... }\`; never write \`result.data.field\` in a test.
106
+ Function return shape: \`gipity fn call\`, the in-test \`ctx.fn.call\`/\`callAs\`, and the client \`Gipity.fn\` all return your function's value **unwrapped** - read/assert \`result.field\`. Only raw HTTP/\`curl\` wraps it as \`{ data: ... }\`; never write \`result.data.field\` in a test.
94
107
 
95
- Tests write to your real DB: \`gipity test\` runs the test code sandboxed, but \`ctx.fn.call\`/\`callAs\` hit your actual deployed functions, which write to the same project database the app reads from rows a test creates persist and surface on the live page. Register \`ctx.cleanup(fn)\` in any write-test to delete what it made; the harness runs every cleanup after the suite (even on failure).
108
+ Tests write to your real DB: \`gipity test\` runs the test code sandboxed, but \`ctx.fn.call\`/\`callAs\` hit your actual deployed functions, which write to the same project database the app reads from - rows a test creates persist and surface on the live page. Register \`ctx.cleanup(fn)\` in any write-test to delete what it made; the harness runs every cleanup after the suite (even on failure).
96
109
 
97
110
  ## Tool output is complete and synchronous
98
111
 
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Local state for `gipity relay`.
3
3
  *
4
- * One file, `~/.gipity/relay.json`, mode 0600:
4
+ * One file, `$GIPITY_DIR/relay.json` (default `~/.gipity/relay.json`), mode 0600:
5
5
  * {
6
6
  * device: { guid, name, platform, token, paired_at },
7
7
  * // (no allowlist - daemon materializes any of the user's projects on demand)
@@ -16,7 +16,13 @@
16
16
  import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync, unlinkSync } from 'fs';
17
17
  import { join } from 'path';
18
18
  import { homedir } from 'os';
19
- const RELAY_DIR = join(homedir(), '.gipity');
19
+ // GIPITY_DIR scopes the relay/device state the same way it scopes auth.json (see
20
+ // auth.ts). Without this, a separate auth context (e.g. GIPITY_DIR=~/.giprunner-prod
21
+ // logged in as ec-giprunner@914-6.com) would still read the DEFAULT ~/.gipity device —
22
+ // which is paired to a DIFFERENT account — and project/chat creation fails with
23
+ // "deviceGuid does not match a paired device". Scoping it lets each auth context pair
24
+ // and own its own device. Unset GIPITY_DIR → ~/.gipity, unchanged for normal users.
25
+ const RELAY_DIR = process.env.GIPITY_DIR || join(homedir(), '.gipity');
20
26
  const RELAY_FILE = join(RELAY_DIR, 'relay.json');
21
27
  const FILE_MODE = 0o600;
22
28
  function emptyState() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gipity",
3
- "version": "1.0.392",
3
+ "version": "1.0.394",
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",